Lesson 05-React Native State Management

State Management Overview

Why State Management?

React Native components are driven by state. As application complexity grows, state may be distributed across multiple components, leading to challenges such as:

  • State Sharing Difficulty: Passing props between components becomes cumbersome.
  • Data Consistency Issues: Multiple components modifying the same state can cause conflicts.
  • Debugging Complexity: Tracking the source of state changes is difficult.

State management tools address these issues through centralized or reactive approaches, improving code maintainability and predictability.

Comparison of Four Approaches

ApproachComplexityUse CaseAdvantagesDisadvantages
ReduxHighLarge apps, global stateStrong predictability, easy debuggingBoilerplate-heavy
MobXMediumMedium apps, reactive needsSimple, minimal codeLess explicit state changes
Context APILowSmall apps, lightweight global stateNative, easy to learnPerformance issues in complex scenarios
React HooksLowLocal state, simple global stateFlexible, fewer dependenciesNot ideal for complex global state

Redux: Global State Management

Basic Concepts and Workflow

Redux is a global state management library based on a unidirectional data flow. Core concepts include:

  • Store: A single state tree storing all data.
  • Action: Plain objects describing state changes.
  • Reducer: Pure functions that update state based on actions.

Data Flow

  1. User triggers an Action.
  2. Reducer processes the Action and returns a new state.
  3. Store updates, and components re-render.

Implementing a Simple Redux Application

We’ll create a counter application.

Installation

npm install redux react-redux

Code Implementation

  1. Create Reducer In src/redux/reducers.js:
const initialState = {
  count: 0,
};

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

export default counterReducer;
  1. Create Store In src/redux/store.js:
import { createStore } from 'redux';
import counterReducer from './reducers';

const store = createStore(counterReducer);

export default store;
  1. Connect Components In App.js:
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { Provider, useSelector, useDispatch } from 'react-redux';
import store from './src/redux/store';

const Counter = () => {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <View style={styles.container}>
      <Text>Count: {count}</Text>
      <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
      <Button title="Decrement" onPress={() => dispatch({ type: 'DECREMENT' })} />
    </View>
  );
};

const App = () => {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default App;

Analysis

  • Provider: Injects the Store into the application.
  • useSelector: Retrieves state from the Store.
  • useDispatch: Dispatches actions to update state.

Simplifying with Redux Toolkit

Redux Toolkit reduces boilerplate code with a simpler API.

Installation

npm install @reduxjs/toolkit

Modified Code In src/redux/counterSlice.js:

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: state => { state.count += 1; },
    decrement: state => { state.count -= 1; },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

In src/redux/store.js:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

const store = configureStore({
  reducer: counterReducer,
});

export default store;

In App.js:

import { increment, decrement } from './src/redux/counterSlice';

// In Counter component
<Button title="Increment" onPress={() => dispatch(increment())} />
<Button title="Decrement" onPress={() => dispatch(decrement())} />

Analysis

  • createSlice: Automatically generates Actions and Reducers.
  • Immutability: Uses Immer internally, allowing direct state modifications.

MobX: Reactive State Management

Basic Concepts and Workflow

MobX is a reactive state management library that automatically updates the UI via an observer pattern. Key concepts:

  • Observable: Trackable state.
  • Action: Operations that modify state.
  • Reaction: Responses to state changes.

Implementing a MobX Example

Installation

npm install mobx mobx-react

Code Implementation

  1. Create Store In src/stores/counterStore.js:
import { makeAutoObservable } from 'mobx';

class CounterStore {
  count = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}

export default new CounterStore();
  1. Connect Components In App.js:
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { observer } from 'mobx-react';
import counterStore from './src/stores/counterStore';

const Counter = observer(() => {
  return (
    <View style={styles.container}>
      <Text>Count: {counterStore.count}</Text>
      <Button title="Increment" onPress={() => counterStore.increment()} />
      <Button title="Decrement" onPress={() => counterStore.decrement()} />
    </View>
  );
});

const App = () => <Counter />;

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default App;

Analysis

  • makeAutoObservable: Automatically makes properties and methods observable.
  • observer: Makes components reactive to state changes.
  • Simplicity: No need for explicit action dispatching.

Context API: Lightweight Global State

Basic Concepts and Use Cases

The Context API is React’s built-in global state management tool, suitable for small applications or scenarios requiring cross-component data sharing.

Implementing a Context API Example

Code Implementation In src/context/CounterContext.js:

import React, { createContext, useState } from 'react';

export const CounterContext = createContext();

export const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </CounterContext.Provider>
  );
};

In App.js:

import React, { useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { CounterContext, CounterProvider } from './src/context/CounterContext';

const Counter = () => {
  const { count, increment, decrement } = useContext(CounterContext);

  return (
    <View style={styles.container}>
      <Text>Count: {count}</Text>
      <Button title="Increment" onPress={increment} />
      <Button title="Decrement" onPress={decrement} />
    </View>
  );
};

const App = () => {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default App;

Analysis

  • createContext: Creates a context object.
  • Provider: Provides state and methods.
  • useContext: Consumes context values.

React Hooks: Local State Management

useState and useReducer

useState is ideal for simple state, while useReducer suits complex logic.

Example Code

import React, { useState, useReducer } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

const initialState = { count: 0 };
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    default: return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [simpleCount, setSimpleCount] = useState(0);

  return (
    <View style={styles.container}>
      <Text>useReducer Count: {state.count}</Text>
      <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
      <Button title="Decrement" onPress={() => dispatch({ type: 'DECREMENT' })} />
      <Text>useState Count: {simpleCount}</Text>
      <Button title="Increment" onPress={() => setSimpleCount(simpleCount + 1)} />
      <Button title="Decrement" onPress={() => setSimpleCount(simpleCount - 1)} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default App;

Analysis

  • useState: Simple and direct, ideal for independent state.
  • useReducer: Similar to Redux, suitable for complex state logic.

Global State with useContext

Combine useContext and useReducer for lightweight global state management.

Example Code In src/hooks/useCounter.js:

import { createContext, useContext, useReducer } from 'react';

const CounterContext = createContext();

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    default: return state;
  }
};

export const CounterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
};

export const useCounter = () => useContext(CounterContext);

In App.js:

import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { CounterProvider, useCounter } from './src/hooks/useCounter';

const Counter = () => {
  const { state, dispatch } = useCounter();

  return (
    <View style={styles.container}>
      <Text>Count: {state.count}</Text>
      <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
      <Button title="Decrement" onPress={() => dispatch({ type: 'DECREMENT' })} />
    </View>
  );
};

const App = () => (
  <CounterProvider>
    <Counter />
  </CounterProvider>
);

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default App;

Analysis

  • Global Scope: Shares state via Context.
  • Modularity: Encapsulates logic in a custom Hook.

Comprehensive Case Study

Requirement Analysis

We’ll create a task list application that:

  • Adds tasks.
  • Displays the task count.
  • Implements solutions using Redux, MobX, and Hooks.

Implementation with Multiple Approaches

Redux Implementation

// src/redux/taskSlice.js
import { createSlice } from '@reduxjs/toolkit';

const taskSlice = createSlice({
  name: 'tasks',
  initialState: { tasks: [] },
  reducers: {
    addTask: (state, action) => { state.tasks.push(action.payload); },
  },
});

export const { addTask } = taskSlice.actions;
export default taskSlice.reducer;

// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import taskReducer from './taskSlice';

export default configureStore({ reducer: taskReducer });

// App.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native';
import { Provider, useSelector, useDispatch } from 'react-redux';
import store from './src/redux/store';
import { addTask } from './src/redux/taskSlice';

const TaskApp = () => {
  const [task, setTask] = useState('');
  const tasks = useSelector(state => state.tasks);
  const dispatch = useDispatch();

  return (
    <View style={styles.container}>
      <Text>Tasks: {tasks.length}</Text>
      <TextInput
        style={styles.input}
        value={task}
        onChangeText={setTask}
        placeholder="Add a task"
      />
      <Button title="Add" onPress={() => { dispatch(addTask(task)); setTask(''); }} />
      <FlatList
        data={tasks}
        renderItem={({ item }) => <Text>{item}</Text>}
        keyExtractor={(item, index) => index.toString()}
      />
    </View>
  );
};

const App = () => (
  <Provider store={store}>
    <TaskApp />
  </Provider>
);

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  input: { borderWidth: 1, padding: 10, marginVertical: 10 },
});

export default App;

MobX Implementation

// src/stores/taskStore.js
import { makeAutoObservable } from 'mobx';

class TaskStore {
  tasks = [];

  constructor() {
    makeAutoObservable(this);
  }

  addTask(task) {
    this.tasks.push(task);
  }
}

export default new TaskStore();

// App.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native';
import { observer } from 'mobx-react';
import taskStore from './src/stores/taskStore';

const TaskApp = observer(() => {
  const [task, setTask] = useState('');

  return (
    <View style={styles.container}>
      <Text>Tasks: {taskStore.tasks.length}</Text>
      <TextInput
        style={styles.input}
        value={task}
        onChangeText={setTask}
        placeholder="Add a task"
      />
      <Button title="Add" onPress={() => { taskStore.addTask(task); setTask(''); }} />
      <FlatList
        data={taskStore.tasks}
        renderItem={({ item }) => <Text>{item}</Text>}
        keyExtractor={(item, index) => index.toString()}
      />
    </View>
  );
});

const App = () => <TaskApp />;

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  input: { borderWidth: 1, padding: 10, marginVertical: 10 },
});

export default App;

Hooks Implementation

// src/hooks/useTasks.js
import { createContext, useContext, useReducer } from 'react';

const TaskContext = createContext();

const taskReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TASK': return { tasks: [...state.tasks, action.payload] };
    default: return state;
  }
};

export const TaskProvider = ({ children }) => {
  const [state, dispatch] = useReducer(taskReducer, { tasks: [] });
  return (
    <TaskContext.Provider value={{ state, dispatch }}>
      {children}
    </TaskContext.Provider>
  );
};

export const useTasks = () => useContext(TaskContext);

// App.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native';
import { TaskProvider, useTasks } from './src/hooks/useTasks';

const TaskApp = () => {
  const [task, setTask] = useState('');
  const { state, dispatch } = useTasks();

  return (
    <View style={styles.container}>
      <Text>Tasks: {state.tasks.length}</Text>
      <TextInput
        style={styles.input}
        value={task}
        onChangeText={setTask}
        placeholder="Add a task"
      />
      <Button title="Add" onPress={() => { dispatch({ type: 'ADD_TASK', payload: task }); setTask(''); }} />
      <FlatList
        data={state.tasks}
        renderItem={({ item }) => <Text>{item}</Text>}
        keyExtractor={(item, index) => index.toString()}
      />
    </View>
  );
};

const App = () => (
  <TaskProvider>
    <TaskApp />
  </TaskProvider>
);

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  input: { borderWidth: 1, padding: 10, marginVertical: 10 },
});

export default App;

Analysis

  • Redux: Clear structure, ideal for scalability.
  • MobX: Concise code, reactive updates.
  • Hooks: Lightweight and flexible, suitable for small to medium applications.

Share your love