useReducer, useMemo, and useCallback

React hooks provide an elegant way to manage state and lifecycle in functional components. Among these hooks, useReducer, useMemo, and useCallback offer powerful tools for handling complex state logic, optimizing performance, and managing dependencies. This blog dives deep into these hooks, explaining their usage with detailed examples.

Table of Contents

  1. Introduction to Hooks

  2. useReducer Hook

    • Introduction

    • Basic Usage

    • Advanced Usage

    • Best Practices

  3. useMemo Hook

    • Introduction

    • Basic Usage

    • Advanced Usage

    • Best Practices

  4. useCallback Hook

    • Introduction

    • Basic Usage

    • Advanced Usage

    • Best Practices

  5. Combining Hooks

  6. Conclusion

1. Introduction to Hooks

React hooks were introduced in React 16.8, bringing state and lifecycle management to functional components. Among the various hooks, useReducer, useMemo, and useCallback are essential for managing complex state logic and optimizing performance.

2. useReducer Hook

Introduction

useReducer is a hook used for managing state in a React component, similar to useState. It is particularly useful for complex state logic where multiple sub-values or a state transition logic is involved. It also makes state management more predictable and easier to debug.

Basic Usage

The useReducer hook accepts a reducer function and an initial state, and returns an array containing the current state and a dispatch function. The reducer function takes the current state and an action, and returns the new state.

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default Counter;

Advanced Usage

useReducer shines with complex state logic. Let's consider a more advanced example: a form with multiple input fields.

import React, { useReducer } from 'react';

const initialState = {
  username: '',
  email: '',
  password: ''
};

function reducer(state, action) {
  switch (action.type) {
    case 'setField':
      return {
        ...state,
        [action.field]: action.value
      };
    case 'reset':
      return initialState;
    default:
      throw new Error();
  }
}

function SignupForm() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleChange = (e) => {
    dispatch({ type: 'setField', field: e.target.name, value: e.target.value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(state);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="username"
        value={state.username}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        type="email"
        name="email"
        value={state.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        type="password"
        name="password"
        value={state.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <button type="submit">Submit</button>
      <button type="button" onClick={() => dispatch({ type: 'reset' })}>
        Reset
      </button>
    </form>
  );
}

export default SignupForm;

Best Practices

  • Keep the reducer function pure: The reducer function should not have side effects. It should only compute and return the new state.

  • Use action types and payloads: Define action types as constants to avoid typos and make the code more readable.

  • Consider performance: For performance-critical applications, consider using useMemo and useCallback to prevent unnecessary re-renders.

3. useMemo Hook

Introduction

useMemo is a hook used to memoize the result of a computation. It returns a memoized value, and recomputes it only when one of its dependencies changes. This can optimize performance by preventing expensive calculations on every render.

Basic Usage

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

function ExpensiveComponent({ count }) {
  const expensiveCalculation = (num) => {
    console.log('Calculating...');
    for (let i = 0; i < 1000000000; i++) {} // simulate expensive calculation
    return num * 2;
  };

  const memoizedValue = useMemo(() => expensiveCalculation(count), [count]);

  return <div>Calculated Value: {memoizedValue}</div>;
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveComponent count={count} />
    </div>
  );
}

export default App;

Advanced Usage

In more complex scenarios, you might have multiple dependencies and need to ensure that the memoized value is recalculated only when necessary.

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

function ProductList({ products }) {
  const sortedProducts = useMemo(() => {
    console.log('Sorting products...');
    return [...products].sort((a, b) => a.name.localeCompare(b.name));
  }, [products]);

  return (
    <ul>
      {sortedProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

function App() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Orange' },
    { id: 3, name: 'Banana' },
  ]);

  const addProduct = () => {
    setProducts([...products, { id: products.length + 1, name: 'Mango' }]);
  };

  return (
    <div>
      <button onClick={addProduct}>Add Product</button>
      <ProductList products={products} />
    </div>
  );
}

export default App;

Best Practices

  • Use only when necessary: useMemo should be used for expensive calculations that could cause performance issues if re-computed on every render.

  • Manage dependencies correctly: Ensure all dependencies that affect the memoized value are included in the dependency array.

  • Combine with useCallback: Often used together with useCallback to optimize both values and functions.

4. useCallback Hook

Introduction

useCallback is a hook used to memoize a function. It returns a memoized version of the callback that only changes if one of its dependencies changes. This can prevent unnecessary re-renders, particularly when passing callbacks to child components.

Basic Usage

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

function Child({ onClick }) {
  console.log('Child render');
  return <button onClick={onClick}>Click me</button>;
}

function App() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={increment} />
    </div>
  );
}

export default App;

Advanced Usage

For more advanced usage, consider a scenario where you need to handle multiple callbacks with different dependencies.

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

function List({ items, onItemClick }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index} onClick={() => onItemClick(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
}

function App() {
  const [items] = useState(['Apple', 'Orange', 'Banana']);
  const [selectedItem, setSelectedItem] = useState(null);

  const handleItemClick = useCallback((item) => {
    setSelectedItem(item);
  }, []);

  return (
    <div>
      <List items={items} onItemClick={handleItemClick} />
      {selectedItem && <p>Selected Item: {selectedItem}</p>}
    </div>
  );
}

export default App;

Best Practices

  • Use for stable references: Use useCallback when passing callbacks to optimized child components that rely on reference equality to avoid unnecessary renders.

  • Manage dependencies correctly: Ensure all dependencies that affect the callback are included in the dependency array.

  • useMemo: Often used together with useMemo to optimize both values and functions.

5. Combining Hooks

Combining useReducer, useMemo, and useCallback can lead to highly optimized and maintainable code, especially in complex applications.

Example: Todo List Application

Let's build a Todo List application that demonstrates the combined usage of useReducer, useMemo, and useCallback.

import React, { useReducer, useMemo, useCallback } from 'react';

// Initial State
const initialState = {
  todos: [],
  filter: 'all',
};

// Reducer Function
function reducer(state, action) {
  switch (action.type) {
    case 'addTodo':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggleTodo':
      return {
        ...state,
        todos: state.todos.map((todo, index) =>
          index === action.index ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'setFilter':
      return { ...state, filter: action.filter };
    default:
      throw new Error();
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const filteredTodos = useMemo(() => {
    switch (state.filter) {
      case 'completed':
        return state.todos.filter((todo) => todo.completed);
      case 'active':
        return state.todos.filter((todo) => !todo.completed);
      default:
        return state.todos;
    }
  }, [state.todos, state.filter]);

  const addTodo = useCallback((text) => {
    dispatch({ type: 'addTodo', todo: { text, completed: false } });
  }, []);

  const toggleTodo = useCallback((index) => {
    dispatch({ type: 'toggleTodo', index });
  }, []);

  const setFilter = useCallback((filter) => {
    dispatch({ type: 'setFilter', filter });
  }, []);

  return (
    <div>
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
      <AddTodoForm onAddTodo={addTodo} />
      <FilterButtons filter={state.filter} onSetFilter={setFilter} />
    </div>
  );
}

function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index} onClick={() => onToggle(index)}>
          {todo.text} {todo.completed ? '(Completed)' : ''}
        </li>
      ))}
    </ul>
  );
}

function AddTodoForm({ onAddTodo }) {
  const [text, setText] = React.useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onAddTodo(text);
    setText('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons({ filter, onSetFilter }) {
  return (
    <div>
      <button disabled={filter === 'all'} onClick={() => onSetFilter('all')}>
        All
      </button>
      <button disabled={filter === 'active'} onClick={() => onSetFilter('active')}>
        Active
      </button>
      <button disabled={filter === 'completed'} onClick={() => onSetFilter('completed')}>
        Completed
      </button>
    </div>
  );
}

export default TodoApp;

In this example:

  • State Management: useReducer manages the state of todos and the current filter.

  • Performance Optimization: useMemo memoizes the filtered todos list to avoid unnecessary recalculations.

  • Callback Optimization: useCallback memoizes the functions to prevent unnecessary re-renders of child components.

6. Conclusion

Understanding and effectively using useReducer, useMemo, and useCallback can significantly enhance your ability to manage complex state logic and optimize performance in React applications. These hooks provide powerful tools for creating more efficient, maintainable, and predictable React components.

  • useReducer: Ideal for managing complex state logic with multiple actions and state transitions.

  • useMemo: Useful for memoizing expensive calculations to prevent unnecessary re-renders.

  • useCallback: Helps in memoizing functions to maintain stable references, avoiding unnecessary re-renders of child components.

By combining these hooks, you can build sophisticated and performant React applications.

Last updated