React Hooks: Exploring
useState, useEffect, and useContext

by Paul Dermody, Director of Technology

April 18, 2023


Share via


React Hooks: Exploring 
useState, useEffect, and useContext

React Hooks are a feature of React that allows us to use state and other React features without writing a class. This article will explore three commonly used hooks: useStateuseEffect, and useContext. We will discuss what they are, how they work, and the best practices you should know to use them more effectively in your React projects.

Introduction to React Hooks

Before diving into the details of these hooks, let's briefly discuss React Hooks and why they were introduced. Hooks are a new way to use React features in function components. They were introduced to address some of the shortcomings of class components, such as the complexity of state management and the inability to reuse code. Hooks allow you to "hook" into React's lifecycle methods and state management system, making it easier to write reusable code, manage complex state, and share logic between components.

useState

The useState hook is used to manage state in function components. It takes an initial value as a parameter and returns an array containing the current state value and a function to update it.

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

In this example, we are initializing a state variable called count with an initial value of 0. The useState hook returns an array containing the current value of count and a function we are calling setCount that can be used to update the value of count.

In this example, our state is a simple integer. However, we could use a JavaScript object with multiple properties as easily. Remember that React will only re-render your component if the object reference changes, not if you change individual properties in the object.

Using a function to initialize state

Note that the useState function is called every time the component is rendered. So if the initial value contains a complex calculation, use the useState alternative that receives a function as a parameter to initialize the state, as follows:

const [count, setCount] = useState(() => calculateInitialValue());

If the function called calculateInitialValue is slow, we can be sure it will only be called the first time the component is rendered.

Changing state

Keep in mind that you must always use the second item in the array returned by useState to update that state. In our example, the second item is called setCount. This is the only way to ensure the change persists until the next render. Changing the state directly might work for the current render, but it will revert back to its previous value before the next render. Using the setCount function also ensures that React knows that the state has changed and will schedule a new render as soon as possible.

The function for changing state has two forms. We can pass it a new value for the state as an argument, or we can pass it a function that returns the new value. For example, the following line of code shows the simplest way to change the value of our count.

setCount(count + 1);

This passes a new value directly into the function which it calculates using the “local” value of count. Note that, even after calling the setCount function in this way, the local value of count does not change immediately. So, if you call it multiple times in your code, the count will only increase in value once.

There is another way to call setCount. You can pass in an arrow function instead of a new value for count. When you do this, React will call the given function to determine the new value and will pass it the latest value of count as a parameter.

setCount(count => count + 1);

Any time we call setCount and pass a function, React will postpone calling that function until it is ready to update the state. This will happen after the current execution of our functional component is finished and has been drawn on the screen.

A third way to call the setCount function is as follows. 

setCount(function(count) { return count + 1 });

Here we are passing a traditional function expression rather than an arrow function but it behaves the same as the arrow function.

As mentioned earlier, the function that you pass as an argument to the setCount will itself be called with an argument which is the latest value of count. Additionally, if you have multiple calls to setCount, it will be called in the order that these calls appear in the code and it will present the expected value of count as an argument in each case. Some developers consider it a best practice to change the state this way precisely because it always knows the latest value of the state.

Further reading

If you have a lot of state to manage and you want to share state between components, we recommend you look into other hooks for managing more complex scenarios like useContextuseReducer, as well as external state management libraries like Redux and Zustand.

useEffect

The useEffect hook is used to manage side effects in function components. Side effects are activities that must run asynchronously or that require some type of clean-up after the component is no longer in use.

Examples of side effects include fetching data, directly modifying the DOM, timers, and setting up event listeners or subscriptions to events external to the component itself.

To use this hook, you must call it and pass a function that implements the side effect you need. For example, to download some data from a remote server, you might add a piece of code like this to your component.

useEffect(() => { 
    fetch("https://server/api/items")
        .then(response => response.json())
        .then(data => setItems(data));
}, []);

As with the calls to change state that we discussed in the previous section, the effect is not executed until after rendering has finished. So, if you require the effect to run immediately and synchronously, then it should not be treated as a side effect.

The code in our example effect contacts the server and requests some JSON data. Once the data has arrived, a call is made to update the React state with setItems(). Notice that this will happen asynchronously, outside the rendering activities of React. Updating the state like this lets React know that the component must be rendered again - which usually happens very soon after the state is updated.

The dependencies argument

You may have noticed a second argument that we passed to the useEffect hook. This is an array of dependencies that tell React when it should execute the effect again. If this array is empty, the effect will be executed only once when the component is first added to the component tree (also known as “mounting”).

This dependency argument is optional, but if you do not provide one, the effect will be executed every time the component renders.

If you include any values in the array, React will execute the effect after any render where those values change. For example, suppose we wanted to include a category in our API call to the server. In this case, the code might look like this:

useEffect(() => { 
    fetch("https://server/api/items?category="+category)
        .then(response => response.json())
        .then(data => setItems(data));
}, [category]);

Here, our query to the server includes a filter called category. If the variable, category, is part of the components state, then every time it changes, React will render the component again and execute the effect once more to download new items.

Clean up

The useEffect hook has one other aspect that it is important to understand. If your effect function returns a value, then React assumes that this is a function that cleans up after the component is no longer used. 

For example, the following code adds an event handler to the DOM and ensures the event handler is removed after the component is removed from the component tree.

useEffect(() => {
    document.addEventListener("click", mouseClickHandler);
    return () => {
        document.removeEventListener("click", mouseClickHandler);
    };
}, []));

In this example, the cleanup code is important so that the handler will not be called after the component is no longer in use.

Further reading

Other hooks related to useEffect are:

  • useQuery - This is specifically for managing data that comes from external sources, enabling you to easily handle errors, duplicates, caching, and other complex behaviors.
  • useLayoutEffect - This is similar to useEffect but, unlike use Effect, this allows you to run effects synchronously.

useContext

The useContext hook is the most complex out of the three hooks we are discussing in this article. This hook provides a way to share state with all of the components in your application or with just a particular branch of your component tree.

Using the useContext hook requires four steps. 

  1. Create a Context object
  2. Create a Provider component
  3. Add the Provider component to your component tree and initialize the state
  4. Use the context in the components that need to access the state

Create a Context object

The first step is the simplest. For best practice, we recommend creating your Context object in its own module in your app and having all interested components import that module when they need it. Creating the context requires two lines of code: 

import { createContext } from “react”;
const CountContext = createContext(); 

We will store a count in this context later. For now, the important thing here is the call to createContext to create a Context object just for our state.

Create a Provider component

The next step is to create a Provider component that contains our state and can be added to the branch of our component tree where we want to share that state.

Here is an example.

export const CountProvider = (props) => {
    const [count, setCount] = useState(0);
    const state = { count, setCount };

    return (
        <CountContext.Provider value={state}>
            {props.children}
        </ShoppingCartContext.Provider>
    );
}

The CountProvider is where our state lives, and you can see from the sample code that we are using the useState hook to manage our count state. 

Notice that our state in this example is not just the count value but also the setCount function so that our components can change the count if needed.

In the next section, we will insert this provider into the appropriate place in our component tree where we expect to use the count, or we can wrap the entire tree if the count is used throughout our application.

Add the Provider to the component tree

Once we have the provider, we need to add it to our component tree in a suitable location that depends on which components need access to that state.

function App() {
  return (
    <CountProvider>
      <MyAppComponents/>
    </CountProvider>
  );
}

In this example, we are adding the CountProvider above all but the topmost App component. This ensures that my whole app has access to the current count value and also access to the setCount function, which is needed to update the count for the rest of the application.

Read and update the count in a child component

Now any component in our app can use and change the count value anywhere in our application. Here is an example component that does just that.

const MyCounter = () => {
    const {count, setCount} = useContext(CountContext)

    return (
        <div>
            <button onClick={() => setCount(count-1)}>-</button>
            <span>{count}</span>
            <button onClick={() => setCount(count+1)}>+</button>
        </div>
    )
}

This component will automatically be re-rendered if any other component changes the count. Also, if the user clicks on one of the buttons in this component, all other components in the application that use the count will be notified.

Conclusion

React Hooks provide a way to use state and other React features in function components. They make it easier to write reusable code, manage complex state, and share logic between components. The useStateuseEffect, and useContext hooks are some of the most commonly used hooks in React and are essential for building complex React applications. Our Front-End Developers can help your team reach its goals.

Learn more about React Hooks in this talk.

FAQs

Can hooks be used in class components?

No, hooks can only be used in function components.

Can I use multiple useState hooks in a single component?

Yes, you can use multiple useState hooks in a single component.

How do I clean up the side effects created by useEffect?

You can return a cleanup function from the useEffect hook, which will be called when the component unmounts, or the dependencies change.

Can I use hooks in React Native?

Yes, hooks can be used in React Native and React web applications.

What is the benefit of using useContext over prop drilling?

Using useContext allows you to avoid prop drilling, making your code cleaner and easier to manage. It also allows you to share data between components without complex prop chains.

 

Photo by Lautaro Andreani

Other Blogs

by Paul Dermody

May 21, 2023

Real Applications of Blockchain Technology

Are you interested in our staff outsourcing services? Check our frequently asked questions to find out more about us.

Our FAQ

Discover our Affiliate Program - Earn commission for your business while also helping companies in your network build their technology dream teams with Stateside.

About Referrals

If, after one month, you're not satisfied with the quality we deliver, we will void the first invoice and terminate the contract free of charge.

Hire Tech Talent