James Bachini

React Hooks Tutorial | The 7 Most Important React Hooks

React Hooks Tutorial

React Hooks provide a way to manage state, side effects, references and more in functional components. In this tutorial, we’ll look at the seven most commonly used React hooks

  1. useState – Manages state, providing a variable and setter function to rerender when state variable is changed
  2. useEffect – Runs a function the first time and every time a component renders
  3. useContext – Lets you use values from a context directly, without needing to pass them down as props
  4. useReducer – Helps manage more complex state logic, like a more powerful version of useState
  5. useCallback – Keeps a function the same between renders, to avoid unnecessary updates
  6. useMemo – Remembers the result of a function so it doesn’t need to be redone unless necessary
  7. useRef – Holds a value that stays the same across renders, often used to directly interact with DOM elements

When You Would Use These React Hooks

useState
Imagine you have a piece of data in your component that might change, like a number for a counter or a form input. useState is like a special tool that helps you keep track of that data. When you change the data, useState automatically rerenders the component so it shows the new data on the screen. It is super simple and perfect for handling basic stuff like toggles, counters, or text inputs.

useEffect
Think of useEffect as a way to tell your component “Do this thing every time you render the component”. It is great for things like grabbing data from an API, setting up timers, or managing event listeners. And if your component goes away (like if you navigate to a different page), useEffect can also clean up things, like turning off timers or unsubscribing from a service. You can even control when it runs by giving it a list of things to watch, so it only runs when those things change.

useContext
Have you ever had to pass the same data through several components just to get it to the one that actually needs it? useContext is like a shortcut for this. Instead of passing props down a long chain of components, you can use useContext to directly access the data wherever you need it. This is super handy when you have global settings, like a theme or user info, that lots of components need to know about.

useReducer
If you find yourself juggling lots of pieces of state and its getting tricky to manage, useReducer is like an upgraded version of useState. It is well suited for handling more complex situations, like when several things need to change together or when one change depends on another. It is especially useful for more complicated forms or processes where lots of things are happening. Think of it like a traffic controller that makes sure all your state changes happen in the right order.

useCallback
Sometimes in React, you have a function that you pass down to other components, but you don’t want that function to change every time the parent component updates. useCallback helps with that. It locks the function in place, so it doesn’t get recreated unless something important changes. This can stop unnecessary rerenders, which is great for keeping your app fast, especially if those child components consume a lot of resources to rerender.

useMemo
If your component does something that takes a lot of processing power, like calculating something big or sorting a large list, useMemo can help. It remembers the result of that operation so it doesn’t have to redo it every time the component updates. It only recalculates if something important changes, which can save a lot of processing time and make your web app run smoother.

useRef
useRef is like a way to directly grab a specific part of your component without causing the whole thing to update. It’s often used for things like focusing on an input field, playing a video, or handling animations. Plus, it can also hold onto values that you dont want to trigger a rerender when they change, like keeping track of how many times something has happened. It is a handy tool for situations where you need to interact with the actual elements in your component.


useState

useState is a hook that allows you to add state to functional components. State in React refers to data that changes over time, such as user input, the result of an API call, or the status of a component (e.g., whether a dropdown is open or closed).

useState Syntax

The useState hook is a function that returns an array with two elements:

  • The current state value.
  • A function to update the state.

Here is the basic syntax:

const [state, setState] = useState(initialState);
  • state = The current state value.
  • setState = A function that updates the state value and triggers a re-render of the component.
  • initialState = The initial value of the state when the component is first rendered.
Basic useState Example

Lets start with a simple example where we manage a counter using useState.

import React, { useState } from 'react';

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

We initialize the state count with 0.

The setCount function is used to update the count state.

When the button is clicked, setCount updates the count state by incrementing it by 1.

React automatically re-renders the component whenever the state changes, displaying the updated count.

Initializing useState with a Function

Sometimes, the initial state is the result of a more complex calculation. In such cases, you can pass a function to useState. This function will be executed only on the first render, making it more efficient.

function calculateInitialValue() {
  return 10;
}

function Counter() {
  const [count, setCount] = useState(calculateInitialValue);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

calculateInitialValue is only called once, during the initial render, to set the initial state.

This is useful when you have a complex or expensive computation for setting the initial state.

Updating State Based on Previous State

When updating state, sometimes you need to base the new state on the previous state. You can do this by passing a function to the state updater.

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>
        Click me
      </button>
    </div>
  );
}

setCount takes a function as an argument, where prevCount is the previous state value.

This pattern is particularly useful when the new state depends on the old state, such as when incrementing a counter.

Handling Multiple State Variables

You can call useState multiple times to manage different pieces of state in the same component. Remember that the component will rerender whenever any one of the state variables changes.

function UserProfile() {
  const [name, setName] = useState('John Doe');
  const [age, setAge] = useState(30);

  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
      <button onClick={() => setAge(age + 1)}>Increase Age</button>
      <button onClick={() => setName('Jane Doe')}>Change Name</button>
    </div>
  );
}

Here, name and age are two independent pieces of state managed by separate useState calls.

Each state variable has its own updater function (setName and setAge).

Using Objects or Arrays as State

You can also store objects or arrays in state. However, when updating them, you should ensure you are not mutating the existing state directly.

function UserProfile() {
  const [user, setUser] = useState({ name: 'John Doe', age: 30 });

  const updateName = () => {
    setUser(prevUser => ({
      ...prevUser,  // Spread the previous state
      name: 'Jane Doe'
    }));
  };

  const increaseAge = () => {
    setUser(prevUser => ({
      ...prevUser,
      age: prevUser.age + 1
    }));
  };

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <button onClick={increaseAge}>Increase Age</button>
      <button onClick={updateName}>Change Name</button>
    </div>
  );
}

user is an object with two properties: name and age.

When updating the state, we use the spread operator (…prevUser) to copy the existing state and then override the properties we want to change.

This ensures that we are not mutating the original state directly, which is a key principle in React.


useEffect

useEffect is a hook that lets you run side effects in your functional components. Side effects are operations that affect something outside the scope of the function, such as fetching data from an API, modifying the DOM, or setting up a subscription.

Basic useEffect Syntax

The useEffect hook takes two arguments:

  • A function that contains the side effect logic
  • An optional dependency array that determines when the effect should run

Here’s the basic syntax:

useEffect(() => {
  // Side effect logic here
  return () => {
    // Cleanup logic here (optional)
  };
}, [dependencies]);

The first argument is a function where you write the code that you want to run after the component renders.

The second argument is an optional array of dependencies. If provided, the effect will re-run only when the values in this array change. If not provided, the effect runs after every render.

Lets start with a simple example that logs a message to the console every time the component renders.

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

function MessageLogger() {
  const [message, setMessage] = useState('Hello, World!');

  useEffect(() => {
    console.log('Component rendered or updated');

    return () => {
      console.log('Cleanup: Component will unmount or update');
    };
  }, [message]);

  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setMessage('Hello, React!')}>Change Message</button>
    </div>
  );
}

export default MessageLogger;

The useEffect hook runs the effect (logging to the console) after every render when the message state changes.

The cleanup function inside the return statement is called just before the component re-renders or unmounts, allowing you to clean up any resources or subscriptions.

Fetching Data with useEffect

A common use case for useEffect is fetching data from an API when a component mounts.

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

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('https://api.jamesbachini.com/data')
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error fetching data:', error);
        setLoading(false);
      });
  }, []); // Empty dependency array means this effect runs once on mount

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <p>Data: {JSON.stringify(data)}</p>
    </div>
  );
}

export default DataFetcher;

We use useEffect to fetch data when the component mounts (since the dependency array is empty, the effect runs only once).

The state is updated with the fetched data, and the loading indicator is hidden once the data is loaded.

Using Multiple useEffect Hooks

You can use multiple useEffect hooks in a single component, each handling different side effects.

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

function MultiEffectComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('React');

  useEffect(() => {
    console.log(`Count has changed: ${count}`);
  }, [count]);

  useEffect(() => {
    console.log(`Name has changed: ${name}`);
  }, [name]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>

      <p>Name: {name}</p>
      <button onClick={() => setName('React Hooks')}>Change Name</button>
    </div>
  );
}

export default MultiEffectComponent;

We have two separate useEffect hooks, each handling a different piece of state.

The first useEffect runs whenever count changes, and the second runs whenever name changes.

Cleaning Up Subscriptions with useEffect

When working with subscriptions or event listeners, it is important to clean up when the component unmounts or before it re-renders to avoid memory leaks.

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

function WindowResizeTracker() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth);

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty dependency array means this effect runs only on mount

  return (
    <div>
      <p>Window width: {windowWidth}px</p>
    </div>
  );
}

export default WindowResizeTracker;

The handleResize function updates the state with the current window width.

The event listener is added when the component mounts and removed when the component unmounts, ensuring there are no memory leaks.

Conditional Effects with Dependencies

Sometimes, you want an effect to run only when certain conditions are met. This can be achieved by carefully controlling the dependency array.

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

function ConditionalEffectComponent() {
  const [count, setCount] = useState(0);
  const [trigger, setTrigger] = useState(false);

  useEffect(() => {
    if (trigger) {
      console.log(`Effect ran because trigger is true and count is: ${count}`);
    }
  }, [count, trigger]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>

      <p>Trigger: {trigger.toString()}</p>
      <button onClick={() => setTrigger(!trigger)}>Toggle Trigger</button>
    </div>
  );
}

export default ConditionalEffectComponent;

The effect runs whenever count or trigger changes, but the actual side effect (logging) only occurs if trigger is true.

This is an example of conditionally running an effect based on state. To summarise:

  • useEffect lets you perform side effects in your components.
  • The dependency array controls when the effect runs.
  • Always clean up after your effects to avoid memory leaks.

useContext

useContext is a React hook that allows you to subscribe to a React context, retrieving the current value of the context without needing to wrap the component with a Consumer component. It’s primarily used for sharing global data like themes, user information, or settings across the entire component tree.

The useContext hook is used as follows:

const value = useContext(MyContext);

MyContext is the context object created by React.createContext. Before using useContext, you need to create a context using React.createContext.

import React, { createContext } from 'react';
const MyContext = createContext('default value');

createContext creates a context object with a default value (‘default value’ in this case).

This default value is used when a component does not have a matching Provider higher up in the tree.

Providing Context to Components

To make the context available to components, wrap them in a Context.Provider.

import React, { createContext } from 'react';

const MyContext = createContext();

function App() {
  return (
    <MyContext.Provider value="Hello from Context!">
      <ComponentA />
    </MyContext.Provider>
  );
}

The value prop in MyContext.Provider is the value that will be passed to consuming components.

Consuming Context with useContext

Now that we have created and provided the context, we can consume it using the useContext hook.

import React, { useContext } from 'react';
import MyContext from './MyContext'; // Assume this is where you created the context

function ComponentA() {
  const value = useContext(MyContext); // Consume the context value

  return <p>{value}</p>;
}

useContext(MyContext) returns the current value of MyContext (“Hello from Context!” in this case).

You can use this value directly in your component.

useContext Example Code

Lets build a more comprehensive example where we create a simple theme toggler using useContext.

Step 1: Create the Context

We will start by creating a ThemeContext to hold our theme data.

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

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

ThemeContext is our context object, created with createContext.

ThemeProvider is a component that uses ThemeContext.Provider to pass down the current theme and a toggleTheme function to toggle between light and dark themes.

Step 2: Using the Context in Components

Now let’s use ThemeContext in our components.

import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      Toggle Theme
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

export default App;

useContext(ThemeContext) is used in the ThemedButton component to access the current theme and the toggleTheme function.

The buttons background color and text color change based on the current theme, and clicking the button toggles between the light and dark themes.

useContext Authentication

Another common use case for useContext is managing user authentication status across an application.

We will create a context to hold the users authentication status and functions for logging in and out.

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

export const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => setIsAuthenticated(true);
  const logout = () => setIsAuthenticated(false);

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

Next, lets use the AuthContext in a component to display different content based on the users authentication status.

import React, { useContext } from 'react';
import { AuthContext } from './AuthContext';

function Navbar() {
  const { isAuthenticated, login, logout } = useContext(AuthContext);

  return (
    <nav>
      <h1>My App</h1>
      {isAuthenticated ? (
        <button onClick={logout}>Logout</button>
      ) : (
        <button onClick={login}>Login</button>
      )}
    </nav>
  );
}

function App() {
  return (
    <AuthProvider>
      <Navbar />
      <div>
        <h2>Welcome to the App</h2>
      </div>
    </AuthProvider>
  );
}

export default App;

Navbar component uses useContext(AuthContext) to determine whether the user is authenticated.

Depending on the isAuthenticated value, it displays either a “Login” or “Logout” button.

Clicking the button toggles the authentication status.

Nested Contexts

Sometimes you may have multiple contexts in your application, useContext makes it easy to use them together.

import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
import { AuthContext } from './AuthContext';

function Dashboard() {
  const { theme } = useContext(ThemeContext);
  const { isAuthenticated } = useContext(AuthContext);

  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      {isAuthenticated ? <h2>Welcome back!</h2> : <h2>Please log in.</h2>}
    </div>
  );
}

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <Dashboard />
      </ThemeProvider>
    </AuthProvider>
  );
}

export default App;

Dashboard component uses both ThemeContext & AuthContext to determine its appearance and content.

This approach allows for very flexible and maintainable code, especially in larger applications with multiple global states.

useContext allows you to access context values directly without needing to pass props through intermediate components. It is ideal for global states like themes, authentication, or configuration settings. Proper use of useContext can simplify your component tree and make your code more maintainable.


useReducer

Reacts useReducer hook is a powerful alternative to useState for managing more complex state logic in functional components. It is particularly useful when you have state transitions based on actions.

The reducer function takes the current state and an action, and returns a new state based on the action. This pattern is common in state management libraries like Redux, but useReducer allows you to implement it within a single component.

useReducer vs useState

Use useState when…

  • Managing simple, standalone state variables
  • State updates are straightforward and independent

Use useReducer when…

  • Managing complex state logic involving multiple sub values
  • The next state depends on the previous state

You find yourself using multiple useState hooks that can be combined into a single reducer.

useReducer Basic Syntax

Here’s the basic syntax for useReducer

const [currentState, dispatchFunction] = useReducer(reducerFunction, initialState);

The useReducer hook takes two arguments:

  • reducerFunction A function that determines how the state should change based on an action.
  • initialState The initial value of the state.

It returns an array with two elements [currentState, dispatchFunction]

  • currentState The current state value.
  • dispatchFunction A function to send actions to the reducer.
useReducer Example Code

Lets start with a simple example of a counter to understand the basics of useReducer

We start by defining the Reducer Function which takes two parameters described above. Based on the action type, it returns a new state.

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(`Unhandled action type: ${action.type}`);
  }
}

We can then use useReducer in a React component

import React, { useReducer } from 'react';

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

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

export default Counter;

We start with { count: 0 } and pass this initial state to the useReducer function.

The reducer handles two action types: ‘increment’ and ‘decrement’, updating the count accordingly.

Dispatch is used to send actions to the reducer, triggering state updates.

Combining useReducer with useContext

For larger applications, you might want to manage global state using useReducer in combination with useContext. This approach centralizes state management and makes it accessible throughout the component tree.

Lets first create the Context and Provider

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

// Initial state
const initialState = {
  todos: [],
};

// Create context
export const TodoContext = createContext();

// Reducer function
function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false },
        ],
      };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    case 'delete':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

// Provider component
export function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);

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

Then we can consume this context within React components

import React, { useContext, useState } from 'react';
import { TodoContext, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useContext(TodoContext);
  const [input, setInput] = useState('');

  const handleAdd = () => {
    if (input.trim() !== '') {
      dispatch({ type: 'add', payload: input });
      setInput('');
    }
  };

  const handleToggle = (id) => {
    dispatch({ type: 'toggle', payload: id });
  };

  const handleDelete = (id) => {
    dispatch({ type: 'delete', payload: id });
  };

  return (
    <div>
      <h2>Global Todo List</h2>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter todo"
      />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {state.todos.map((todo) => (
          <li key={todo.id}>
            <span
              onClick={() => handleToggle(todo.id)}
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
                cursor: 'pointer',
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => handleDelete(todo.id)} style={{ marginLeft: '10px' }}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

function App() {
  return (
    <TodoProvider>
      <TodoList />
    </TodoProvider>
  );
}

export default App;

TodoContext provides a global context for the todo state and dispatch function. TodoProvider wraps the application (or part of it) to provide access to the todo state. TodoList consumes the context using useContext(TodoContext) to access state and dispatch, managing todos globally.

The useReducer hook is a useful alternative to useState for managing complex state logic in React components. It shines in scenarios where you need to manage multiple state variables or when the next state depends heavily on the previous one.

By separating state management logic into a reducer function, useReducer helps keep your components clean, organized, and easier to maintain.


useCallback

The useCallback hook helps you avoid unnecessary recreations of functions during rerenders, which can improve the performance of your application, especially when passing functions as props to child components.

useCallback returns a memoized version of a callback function. This means that the function is only recreated if one of the dependencies has changed. By preventing the function from being recreated on every render, you can avoid unnecessary rerenders of child components that depend on that function.

useCallback is widely used to…

Pass functions as Props
If you pass a function as a prop to a child component, useCallback can prevent unnecessary re-renders by ensuring that the same function instance is passed unless dependencies change.

Prevent ReRender Operations
When you have functions that perform computationally expensive calculations and are called multiple times, useCallback can help prevent recalculations unless necessary.

Optimise React.memo
When using React.memo to prevent re-renders of child components, useCallback can ensure that the props (functions) remain stable across renders.

Basic useCallback Syntax

The useCallback hook is used as follows:

const myCallback = useCallback(() => {
  // Foobar function logic
}, [dependencies]);

The dependencies variable is an array of dependencies that determine when the function should be recreated. If any value in this array changes, the function will be recreated.

useCallback Example Code

Here is a simple example where we use useCallback to prevent unnecessary recreation of a function.

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

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

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // No dependencies, function is created only once

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

export default Counter;

The increment function is wrapped in useCallback with an empty dependency array ([]). This means the increment function is only created once and will not be recreated on every render.

This setup is simple, but in more complex scenarios where increment might be passed down as a prop, using useCallback ensures that the function instance remains stable.

useCallback Dependencies

In this example, we will explore how useCallback works with dependencies. The function will only be recreated when one of its dependencies changes.

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

function CounterWithMultiplier() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  const multiplyCount = useCallback(() => {
    setCount((prevCount) => prevCount * multiplier);
  }, [multiplier]); // The function is re-created when `multiplier` changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setMultiplier(multiplier + 1)}>
        Increase Multiplier
      </button>
      <button onClick={multiplyCount}>Multiply Count</button>
    </div>
  );
}

export default CounterWithMultiplier;

The multiplyCount function depends on the multiplier state. Therefore, it’s wrapped in useCallback with [multiplier] as the dependency.

The function will be recreated whenever multiplier changes. This ensures that multiplyCount always has the latest multiplier value without unnecessary recreations when multiplier doesn’t change.

useCallback and memo

By using React.memo functions, we can prevent unnecessary rerenders, making components more efficient. However, it’s important to use useCallback judiciously, focusing on scenarios where function recreation leads to performance bottlenecks.

In this example, we’ll demonstrate how useCallback works in conjunction with memo to optimize the rendering of child components.

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

const ChildButton = memo(({ onClick, children }) => {
  console.log('ChildButton re-rendered');
  return <button onClick={onClick}>{children}</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // Function does not depend on `text`, so no need to re-create

  return (
    <div>
      <p>Count: {count}</p>
      <ChildButton onClick={handleClick}>Increment Count</ChildButton>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type something"
      />
    </div>
  );
}

export default ParentComponent;

The ChildButton component is wrapped in React.memo, which prevents it from rerendering unless its props change.

The useCallback hook ensures that the handleClick function is only recreated if its dependencies change. In this case, there are no dependencies, so the function is created once.

Since handleClick does not change across renders, ChildButton does not rerender when ParentComponent rerenders due to changes in the text state. This improves performance by avoiding unnecessary renders.

The useCallback hook is a valuable tool in the React developers toolbox, especially for optimising performance in complex applications. Combined with memo, useCallback can significantly enhance the performance of your React applications, leading to a smoother user experience.


useMemo

The useMemo hook allows you to prevent unnecessary recalculations of values that don’t need to change unless specific dependencies do. In this tutorial, we’ll explore useMemo in depth, covering its syntax, practical examples, and best practices.

useMemo memoizes the result of a calculation or function, recomputing the value only when one of its dependencies changes. This can significantly improve performance in your React applications by avoiding unnecessary and potentially expensive recalculations during rerenders.

useMemo is widely used when you have a computationally heavy operation that only needs to be recalculated when certain data changes to avoid unnecessary recalculations or rerenders of child components. It can also ensure that a stable reference is maintained for objects or arrays passed to child components or as dependencies in other hooks like useEffect.

useMemo is particularly useful for ensuring referential equality when passing objects or arrays as props or when using them in other hooks like useEffect or useCallback.

Basic useMemo Syntax

The useMemo hook is used as follows:

import React, { useMemo } from 'react';

const memoValue = useMemo(() => {
    // Foobar function returns computedValue; 
}, [dependencies]);

Dependencies again are an array of dependencies that determine when the function should be reexecuted. If any value in this array changes, the memo value is recalculated.

useMemo Example Code

Let’s start with a simple example where useMemo is used to optimise a calculation.

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

function CalculationComponent() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  const Calculation = (num) => {
    console.log('Calculating...');
    for (let i = 0; i < 1000000000; i++) {
      num += 1;
    }
    return num;
  };

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

  return (
    <div>
      <h2> Calculation with Memoization</h2>
      <p>Count: {count}</p>
      <p>Result of  Calculation: {memoizedValue}</p>
      <button onClick={() => setCount(count + increment)}>Increment Count</button>
      <button onClick={() => setIncrement(increment + 1)}>Increase Increment</button>
    </div>
  );
}

export default CalculationComponent;

The useMemo hook wraps the Calculation function. It recalculates the result only when count changes, avoiding unnecessary recalculations when increment changes.

Without useMemo, the expensive calculation would run every time the component re-renders, which could degrade performance.

In this next example, we’ll use useMemo to optimiwe the filtering of a large list of items based on user input.

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

function FilteredList() {
  const [query, setQuery] = useState('');
  const [items] = useState([
    'apple',
    'banana',
    'cherry',
    'date',
    'elderberry',
    'fig',
    'grape',
    'honeydew',
  ]);

  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter((item) =>
      item.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, items]);

  return (
    <div>
      <h2>Filtered List</h2>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {filteredItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default FilteredList;

The filteredItems array is created by filtering the original items array based on the users search query.

The useMemo hook ensures that the filtering logic only runs when query or items change, preventing unnecessary recalculations when the input is stable.

This optimisation can improve the user experience by making the UI more responsive, especially with large lists.

Lets explore how useMemo can be used to prevent unnecessary rerenders in child components.

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

const ChildComponent = memo(({ data }) => {
  console.log('ChildComponent re-rendered');
  return <div>{data}</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const data = useMemo(() => {
    return { value: count };
  }, [count]); // Memoized to prevent re-creation unless count changes

  return (
    <div>
      <h2>Parent Component</h2>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type something"
      />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <ChildComponent data={data} />
    </div>
  );
}

export default ParentComponent;

The ChildComponent is memoized using React.memo, which prevents it from re-rendering unless its props change.

The data object is memoized with useMemo to ensure it only changes when count changes, not when text changes.

Be careful to include all necessary dependencies in the dependency array. Missing a dependency can lead to stale values being used, resulting in bugs.

These examples demonstrate how the useMemo hook is useful in scenarios where we are dealing with heavy computations, filtering large data sets, or ensuring stable references for objects or arrays passed to child components.


useRef

The useRef hook allows you to create and manage references to DOM elements or persist values across renders without causing a rerender.

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). This ref object persists across rerenders and does not cause the component to rerender when it is changed.

Accessing DOM Elements: When you need to directly interact with a DOM element, such as focusing an input field or measuring an elements dimensions.

useRef is used when you want to store values across renders without causing a rerender, such as tracking a previous state or storing a timeout ID.

Basic useRef Syntax

The useRef hook is used as follows…

const refContainer = useRef(initialValue);

refContainer = The object returned by useRef. It contains a single property, .current, which holds the reference value.

initialValue = The initial value you want the .current property to hold.

One of the most common uses of useRef is to directly access and interact with DOM elements.

import React, { useRef } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    // Use the ref to focus the input element
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Focus me!" />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
}

export default FocusInput;

useRef(null) creates a ref object initialized with null. The input elements ref attribute is set to inputRef, linking the DOM element to the useRef object. The handleFocus function uses inputRef.current.focus() to programmatically focus the input element when the button is clicked.

useRef Example Code

useRef can also be used to persist values across renders, such as storing a previous state or keeping track of a value that should not trigger a rerender.

import React, { useState, useRef, useEffect } from 'react';

function PreviousStateTracker() {
  const [count, setCount] = useState(0);
  const previousCountRef = useRef();

  useEffect(() => {
    // Store the current count in the ref
    previousCountRef.current = count;
  }, [count]); // Update the ref whenever count changes

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {previousCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default PreviousStateTracker;

The previousCountRef ref stores the value of count from the previous render. useEffect is used to update previousCountRef.current whenever count changes.

previousCountRef.current holds the value of count from the last render, allowing you to compare the current state with the previous one.

In some cases, you might want to store a value that persists across renders but should not cause a re-render when updated. useRef is perfect for this scenario.

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

function Timer() {
  const [count, setCount] = useState(0);
  const timerRef = useRef(null);

  const startTimer = () => {
    if (!timerRef.current) {
      timerRef.current = setInterval(() => {
        setCount((prevCount) => prevCount + 1);
      }, 1000);
    }
  };

  const stopTimer = () => {
    clearInterval(timerRef.current);
    timerRef.current = null;
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start Timer</button>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

export default Timer;

timerRef is used to store the ID of the interval timer created by setInterval. Updating timerRef.current does not trigger a rerender, so the components rendering logic remains unaffected.

The startTimer and stopTimer functions manage the timer using the stored interval ID in timerRef.current.

useRef for Mutable Values

useRef can be used to store mutable values that need to be updated frequently without causing re-renders.

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

function ClickTracker() {
  const [clicks, setClicks] = useState(0);
  const lastClickTimeRef = useRef(null);

  const handleClick = () => {
    const now = new Date();
    if (lastClickTimeRef.current) {
      const timeSinceLastClick = now - lastClickTimeRef.current;
      console.log(`Time since last click: ${timeSinceLastClick}ms`);
    }
    lastClickTimeRef.current = now;
    setClicks((prevClicks) => prevClicks + 1);
  };

  return (
    <div>
      <p>Clicks: {clicks}</p>
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

export default ClickTracker;

lastClickTimeRef stores the timestamp of the last button click.

The difference between the current time and the last click time is logged without causing a rerender.

By storing the last click time in useRef, you avoid the overhead of rerendering the component every time the button is clicked.

useRef with useEffect

In more complex scenarios, useRef is often combined with useEffect to perform DOM manipulations after a component has rendered.

import React, { useEffect, useRef } from 'react';

function ScrollToTopButton() {
  const buttonRef = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      if (window.scrollY > 200) {
        buttonRef.current.style.display = 'block';
      } else {
        buttonRef.current.style.display = 'none';
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  const scrollToTop = () => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  return (
    <button
      ref={buttonRef}
      onClick={scrollToTop}
      style={{ display: 'none', position: 'fixed', bottom: '10px', right: '10px' }}
    >
      Scroll to Top
    </button>
  );
}

export default ScrollToTopButton;

The ScrollToTopButton component displays a button when the user scrolls down the page. The buttonRef is used to directly manipulate the buttons style property based on the scroll position. useEffect sets up a scroll event listener to manage the buttons visibility.

Note that direct DOM manipulation should be minimised in React to keep components declarative and consistent with Reacts rendering model. Use useRef for DOM manipulation, but prefer Reacts built in methods for managing component states and props.

The useRef hook is ideal for accessing and manipulating DOM elements, storing mutable values, and persisting values across renders without causing re-renders.


With these React hook code examples, you should be well equipped to use React hooks in your applications, enhancing their functionality and performance.

React web development performance


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.


Posted

in

, , , ,

by