Skip to content

React State Management Redux

Overview

Redux is a predictable state container for managing application-wide state. This chapter will learn about Redux's core concepts, integration with React, and modern Redux Toolkit usage.

🏪 Redux Core Concepts

Basic Redux Setup

jsx
// Simulate Redux functionality
function createStore(reducer, initialState) {
  let state = initialState;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  };

  return { getState, dispatch, subscribe };
}

// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_COUNT = 'SET_COUNT';

// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });
const setCount = (count) => ({ type: SET_COUNT, payload: count });

// Reducer
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 };
    case DECREMENT:
      return { ...state, count: state.count - 1 };
    case RESET:
      return { ...state, count: 0 };
    case SET_COUNT:
      return { ...state, count: action.payload };
    default:
      return state;
  }
}

// React Redux connection
const ReduxContext = React.createContext();

function Provider({ store, children }) {
  const [, forceUpdate] = React.useReducer(x => x + 1, 0);

  React.useEffect(() => {
    const unsubscribe = store.subscribe(forceUpdate);
    return unsubscribe;
  }, [store]);

  return (
    <ReduxContext.Provider value={store}>
      {children}
    </ReduxContext.Provider>
  );
}

function useSelector(selector) {
  const store = React.useContext(ReduxContext);
  return selector(store.getState());
}

function useDispatch() {
  const store = React.useContext(ReduxContext);
  return store.dispatch;
}

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

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>Redux Counter</h2>
      <div style={{ fontSize: '48px', margin: '20px 0', color: '#007bff' }}>
        {count}
      </div>
      <div>
        <button
          onClick={() => dispatch(decrement())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          -1
        </button>
        <button
          onClick={() => dispatch(increment())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          +1
        </button>
        <button
          onClick={() => dispatch(reset())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          Reset
        </button>
      </div>
    </div>
  );
}

function BasicReduxExample() {
  const store = React.useMemo(() => createStore(counterReducer), []);

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

Complex State Management

jsx
// Todo Actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';

const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: Date.now(), text, completed: false }
});

const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: id
});

const deleteTodo = (id) => ({
  type: DELETE_TODO,
  payload: id
});

const setFilter = (filter) => ({
  type: SET_FILTER,
  payload: filter
});

// Todo Reducer
function todosReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case DELETE_TODO:
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

// Filter Reducer
function filterReducer(state = 'ALL', action) {
  switch (action.type) {
    case SET_FILTER:
      return action.payload;
    default:
      return state;
  }
}

// Root Reducer
function rootReducer(state = {}, action) {
  return {
    todos: todosReducer(state.todos, action),
    filter: filterReducer(state.filter, action)
  };
}

// Todo component
function TodoApp() {
  const todos = useSelector(state => state.todos || []);
  const filter = useSelector(state => state.filter || 'ALL');
  const dispatch = useDispatch();

  const [inputValue, setInputValue] = React.useState('');

  const filteredTodos = todos.filter(todo => {
    switch (filter) {
      case 'ACTIVE':
        return !todo.completed;
      case 'COMPLETED':
        return todo.completed;
      default:
        return true;
    }
  });

  const handleAddTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch(addTodo(inputValue.trim()));
      setInputValue('');
    }
  };

  const todoStats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h2>Redux Todo List</h2>

      {/* Add todo */}
      <form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Add new todo..."
          style={{
            padding: '10px',
            width: '70%',
            border: '1px solid #ddd',
            borderRadius: '4px 0 0 4px'
          }}
        />
        <button
          type="submit"
          style={{
            padding: '10px 20px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '0 4px 4px 0'
          }}
        >
          Add
        </button>
      </form>

      {/* Filters */}
      <div style={{ marginBottom: '20px' }}>
        {['ALL', 'ACTIVE', 'COMPLETED'].map(filterType => (
          <button
            key={filterType}
            onClick={() => dispatch(setFilter(filterType))}
            style={{
              margin: '0 5px',
              padding: '8px 16px',
              backgroundColor: filter === filterType ? '#007bff' : '#f8f9fa',
              color: filter === filterType ? 'white' : '#333',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          >
            {filterType === 'ALL' ? 'All' : filterType === 'ACTIVE' ? 'Active' : 'Completed'}
          </button>
        ))}
      </div>

      {/* Statistics */}
      <div style={{
        backgroundColor: '#f8f9fa',
        padding: '15px',
        borderRadius: '8px',
        marginBottom: '20px'
      }}>
        <div>Total: {todoStats.total} | Active: {todoStats.active} | Completed: {todoStats.completed}</div>
      </div>

      {/* Todo list */}
      <div>
        {filteredTodos.length === 0 ? (
          <div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
            {filter === 'ALL' ? 'No todos yet' : 'No matching todos'}
          </div>
        ) : (
          filteredTodos.map(todo => (
            <div
              key={todo.id}
              style={{
                display: 'flex',
                alignItems: 'center',
                padding: '12px',
                margin: '8px 0',
                backgroundColor: 'white',
                border: '1px solid #ddd',
                borderRadius: '8px',
                boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
              }}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => dispatch(toggleTodo(todo.id))}
                style={{ marginRight: '12px' }}
              />
              <span
                style={{
                  flex: 1,
                  textDecoration: todo.completed ? 'line-through' : 'none',
                  color: todo.completed ? '#6c757d' : '#333'
                }}
              >
                {todo.text}
              </span>
              <button
                onClick={() => dispatch(deleteTodo(todo.id))}
                style={{
                  backgroundColor: '#dc3545',
                  color: 'white',
                  border: 'none',
                  padding: '6px 12px',
                  borderRadius: '4px',
                  cursor: 'pointer'
                }}
              >
                Delete
              </button>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

function ComplexReduxExample() {
  const store = React.useMemo(() => createStore(rootReducer), []);

  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

🛠️ Redux Toolkit Modern Approach

Slice and Modern Redux

jsx
// Simulate Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
  const actionTypes = {};
  const actionCreators = {};

  Object.keys(reducers).forEach(key => {
    const type = `${name}/${key}`;
    actionTypes[key] = type;
    actionCreators[key] = (payload) => ({ type, payload });
  });

  const reducer = (state = initialState, action) => {
    const reducerKey = Object.keys(actionTypes).find(
      key => actionTypes[key] === action.type
    );

    if (reducerKey && reducers[reducerKey]) {
      return reducers[reducerKey](state, action);
    }

    return state;
  };

  return {
    name,
    reducer,
    actions: actionCreators
  };
}

// User Slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    profile: null,
    loading: false,
    error: null
  },
  reducers: {
    setLoading: (state, action) => ({
      ...state,
      loading: action.payload
    }),
    setProfile: (state, action) => ({
      ...state,
      profile: action.payload,
      loading: false,
      error: null
    }),
    setError: (state, action) => ({
      ...state,
      error: action.payload,
      loading: false
    }),
    clearUser: (state) => ({
      ...state,
      profile: null,
      error: null
    })
  }
});

// Cart Slice
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    total: 0
  },
  reducers: {
    addItem: (state, action) => {
      const existingItem = state.items.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      return state;
    },
    removeItem: (state, action) => {
      state.items = state.items.filter(item => item.id !== action.payload);
      state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      return state;
    },
    updateQuantity: (state, action) => {
      const item = state.items.find(item => item.id === action.payload.id);
      if (item) {
        item.quantity = action.payload.quantity;
        if (item.quantity <= 0) {
          state.items = state.items.filter(i => i.id !== item.id);
        }
      }
      state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      return state;
    },
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
      return state;
    }
  }
});

// Combine Reducer
function combineReducers(slices) {
  return (state = {}, action) => {
    const newState = {};
    Object.keys(slices).forEach(key => {
      newState[key] = slices[key].reducer(state[key], action);
    });
    return newState;
  };
}

const rootReducerToolkit = combineReducers({
  user: userSlice,
  cart: cartSlice
});

// E-commerce app component
function ECommerceApp() {
  const user = useSelector(state => state.user);
  const cart = useSelector(state => state.cart);
  const dispatch = useDispatch();

  const products = [
    { id: 1, name: 'iPhone 15', price: 5999 },
    { id: 2, name: 'MacBook Pro', price: 12999 },
    { id: 3, name: 'AirPods Pro', price: 1899 }
  ];

  const handleLogin = () => {
    dispatch(userSlice.actions.setLoading(true));
    setTimeout(() => {
      dispatch(userSlice.actions.setProfile({
        id: 1,
        name: 'John',
        email: 'john@example.com'
      }));
    }, 1000);
  };

  const handleLogout = () => {
    dispatch(userSlice.actions.clearUser());
    dispatch(cartSlice.actions.clearCart());
  };

  return (
    <div style={{ padding: '20px' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #ddd'
      }}>
        <h1>Redux Toolkit E-commerce Demo</h1>
        <div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
          {user.profile ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
              <span>Welcome, {user.profile.name}</span>
              <button onClick={handleLogout}>Logout</button>
            </div>
          ) : (
            <button onClick={handleLogin} disabled={user.loading}>
              {user.loading ? 'Logging in...' : 'Login'}
            </button>
          )}
          <div style={{
            backgroundColor: '#007bff',
            color: 'white',
            padding: '8px 12px',
            borderRadius: '20px'
          }}>
            Cart: {cart.items.length} items ¥{cart.total}
          </div>
        </div>
      </header>

      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '30px' }}>
        {/* Product list */}
        <div>
          <h2>Product List</h2>
          <div style={{ display: 'grid', gap: '15px' }}>
            {products.map(product => (
              <div key={product.id} style={{
                padding: '20px',
                border: '1px solid #ddd',
                borderRadius: '8px',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center'
              }}>
                <div>
                  <h3 style={{ margin: '0 0 5px 0' }}>{product.name}</h3>
                  <p style={{ margin: 0, color: '#007bff', fontSize: '18px', fontWeight: 'bold' }}>
                    ¥{product.price}
                  </p>
                </div>
                <button
                  onClick={() => dispatch(cartSlice.actions.addItem(product))}
                  style={{
                    backgroundColor: '#28a745',
                    color: 'white',
                    border: 'none',
                    padding: '10px 20px',
                    borderRadius: '6px'
                  }}
                >
                  Add to Cart
                </button>
              </div>
            ))}
          </div>
        </div>

        {/* Shopping cart */}
        <div>
          <h2>Shopping Cart</h2>
          {cart.items.length === 0 ? (
            <div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
              Cart is empty
            </div>
          ) : (
            <div>
              {cart.items.map(item => (
                <div key={item.id} style={{
                  padding: '15px',
                  border: '1px solid #ddd',
                  borderRadius: '8px',
                  marginBottom: '10px'
                }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                    <div>
                      <h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
                      <p style={{ margin: 0, color: '#666' }}>¥{item.price} x {item.quantity}</p>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
                      <button
                        onClick={() => dispatch(cartSlice.actions.updateQuantity({
                          id: item.id,
                          quantity: item.quantity - 1
                        }))}
                        style={{ padding: '5px 10px' }}
                      >
                        -
                      </button>
                      <span>{item.quantity}</span>
                      <button
                        onClick={() => dispatch(cartSlice.actions.updateQuantity({
                          id: item.id,
                          quantity: item.quantity + 1
                        }))}
                        style={{ padding: '5px 10px' }}
                      >
                        +
                      </button>
                      <button
                        onClick={() => dispatch(cartSlice.actions.removeItem(item.id))}
                        style={{ color: 'red', background: 'none', border: 'none' }}
                      >
                        Delete
                      </button>
                    </div>
                  </div>
                </div>
              ))}
              <div style={{
                padding: '15px',
                backgroundColor: '#f8f9fa',
                borderRadius: '8px',
                textAlign: 'center',
                fontSize: '18px',
                fontWeight: 'bold'
              }}>
                Total: ¥{cart.total}
              </div>
              <button
                onClick={() => dispatch(cartSlice.actions.clearCart())}
                style={{
                  width: '100%',
                  padding: '15px',
                  backgroundColor: '#dc3545',
                  color: 'white',
                  border: 'none',
                  borderRadius: '8px',
                  marginTop: '10px',
                  fontSize: '16px'
                }}
              >
                Clear Cart
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function ReduxToolkitExample() {
  const store = React.useMemo(() => createStore(rootReducerToolkit), []);

  return (
    <Provider store={store}>
      <ECommerceApp />
    </Provider>
  );
}

📝 Chapter Summary

Through this chapter, you should have mastered:

Redux Core Concepts

  • ✅ Role of Store, Action, Reducer
  • ✅ Unidirectional data flow and state immutability
  • ✅ React-Redux connection methods
  • ✅ Modern Redux Toolkit usage

Best Practices

  1. State structure design: Keep state flat and normalized
  2. Action design: Use descriptive action types
  3. Reducer pure functions: Avoid side effects and state mutations
  4. Performance optimization: Use selector optimization and memo
  5. Development tools: Use Redux DevTools for debugging

Continue Learning: Next Chapter - React Build and Deployment

Content is for learning and research only.