fbpx Skip to content

Aquent | DEV6

State Management with React Hooks – Part 2

Written by: Ali Hamoody

Introduction

In Part 1, we talked about managing state locally in a React function component, by using the useState and useReducer hooks. It is recommended that you read Part 1. If you haven’t already, you can find it here.

In Part 2, we will expand our state management solution to span multiple components.

Sharing state between related components

Let’s pick up from where we left off in Part 1. We built a tiny application that used the useState and useReducer hooks to manage state in a single component.

Have a quick look at it here, then come back to continue.

Note how I used the useState hook to create the “name” state, then used it and its related “setter” function in an input field.

const [name, setName] = useState("");
<input
  value={name}
  onChange={event => setName(event.target.value)}
/>

The “name” is used as a value for the input field, while the “setName” function is used in the event handler when the input field changes to set the new value for the “name” state.

Passing State between Components

Now let’s talk about how to pass the state down or up to a related component.

Passing the state down

This one is straight-forward and you probably know it already. To pass the state from a parent down to a child component, use React props. For example, pass the “name” value down in a prop.

That’s it really. Nothing special. Let’s move on.

Passing the state up

The best practice is to lift the state up (i.e. define it in the parent component) as needed, then pass it down through props.

Now, if the child component needs to update the state and send the change upstream, we need the help of a callback function. This is a function we create at the parent level and pass down to the child as a prop. An example of such function is the setter function “setName” or the dispatcher “loginDispatch”.

Let’s look at some code to illustrate passing the state down and up.

I will modify the previous example by moving the input field for the name along with its label into a new component called <NameForm> while preserving the existing functionality. Remember, our application also contains a useReducer hook and some login/logout stateful logic. My goal is to keep that intact, while refactoring the code.

Here is the new <NameForm> component:

const NameForm = ({ name, setName }) => {
  return (
    <>
      <label>Name: </label>
      <input value={name} onChange={event => setName(event.target.value)} />
    </>
  );
};

It includes the label and input field, and receives the state value “name” and setter function “setName” as props.

In the <App> component, we substitute the label and input field with the new component. Now it looks like this:

...
<>
  <NameForm name={name} setName={setName} />
  <button
    type="button"
    onClick={() => loginDispatch({ type: "LOGIN", userName: name })}
  >
    Login
  </button>
  <h4>You typed {name}</h4>
</>
...

The result:

  • We passed the “name” state down from <App> to <NameForm> as a prop.
  • We allowed <NameForm> to change the value of the “name” (passing value up) by passing down the “setName” setter function.

You can see the completed code here.

Global State Management

In this section, I will explain how you can store the state globally and use it in any component throughout the application.

Before we begin, let’s refactor our application into multiple React function components, to better illustrate the use case of multiple components using some global state.

There is a header section that displays the logged-in user name and other stateful info. I moved it to a new component called <HeaderInfo> that looks like this:

const HeaderInfo = () => (
  <div className="LoginInfo">
    {loginState.isLoggedIn ? (
      <>
        <div>Welcome {loginState.userName}</div>
        <div>Logged in at {loginState.loginTime}</div>
      </>
    ) : (
      <>
        <div>Guest</div>
        <div>Please login</div>
      </>
    )}
  </div>
);

Notice that the loginState is not defined in this scope. We’ll attend to this shortly.

Similarly, created a LoginForm and LogoutForm:

const LoginForm = () => {
  const [name, setName] = useState("");
  return (
    <>
      <NameForm name={name} setName={setName} />
      <button
        type="button"
        onClick={() => loginDispatch({ type: "LOGIN", userName: name })}
      >
        Login
      </button>
      <h4>You typed {name}</h4>
    </>
  );
};
const LogoutForm = () => (
  <button type="button" onClick={() => loginDispatch({ type: "LOGOUT" })}>
    Logout
  </button>
);

As a result, the <App> component is now simply:

const App = () => {
  const [loginState, loginDispatch] = useReducer(loginReducer, initState);

  return (
    <div className="App">
      <HeaderInfo />
      <hr />
      <div>
        {!loginState.isLoggedIn && <LoginForm />}
        {loginState.isLoggedIn && <LogoutForm />}
      </div>
    </div>
  );
};

The “loginState” and “loginDispatch” are defined in <App> and not available (for now) in the scope of the child components. We can easily pass these two down to child components as props, and that takes care of everything. Right?

This is not a trick question. The answer is yes.

But… that’s not why we’re here!

We will pretend that the application is more complex, larger, with a multi-level component structure and we want to avoid passing props down through multiple levels of components unnecessarily, a phenomenon known as “Prop Drilling”.

Instead, I use the React Context API and the useContext hook to make “loginState” and “loginDispatch” available to other components.

Context is React’s way of providing state values to multiple components across your application or part of it without explicitly passing props.

If you’re familiar with the dependency injection design pattern that is popular in other frameworks, React Context solves some similar problems to a dependency injection framework. You can use it to share global data between components, and it’s built right in to React!

Here is how it works:

1. Context has a “provider” that stores the value of your state and provides it to React components that are descendants of that provider component.

To use it, the syntax is:

<ContextName.Provider value={yourStateValue}>{children}</ContextName.Provider>

2. When a child component needs to access the state, it uses a hook called useContext.

The syntax is:

const yourStateValue = useContext(ContextName)

You can create as many Contexts at any component level you need. Contexts are not restricted to the global scope level.

You can also define the context in a separate JavaScript file for modularity and import it in other components as needed.

Let’s create the new Context and call it LoginContext.

Import createContext and use it to create the context.

import { createContext } from "react";
const LoginContext = createContext();

This code should be placed before component definitions, and not inside any component.

If you use a separate JavaScript file for the context, then this code goes into that file.

Next define the Context Provider at the <App> level and load it with the state value:

const App = () => {
  const [loginState, loginDispatch] = useReducer(loginReducer, initState);

  return (
    <LoginContext.Provider value={[loginState, loginDispatch]}>
      <div className="App">
        <HeaderInfo />
        <hr />
        <div>
          {!loginState.isLoggedIn && <LoginForm />}
          {loginState.isLoggedIn && <LogoutForm />}
        </div>
      </div>
    </LoginContext.Provider>
  );
};

We placed the provider at the top most level of our application. This means all components inside the application have access to it.

Note the state we are storing in this context is an array of two elements, first is the loginState itself, and second is the dispatcher function that we can use to update the state.

Alright, now we are ready to use (or consume) the Context value.

Import the useContext hook:

import { useContext } from "react";

Remember the components we created earlier like <HeaderInfo> that had undefined loginState? Now it’s time to fix that:

const HeaderInfo = () => {
  
  const [loginState, loginDispatch] = useContext(LoginContext);
  return (
    <div className="LoginInfo">
      {loginState.isLoggedIn ? (
        <>
          <div>Welcome {loginState.userName}</div>
          <div>Logged in at {loginState.loginTime}</div>
        </>
      ) : (
        <>
          <div>Guest</div>
          <div>Please login</div>
        </>
      )}
    </div>
  );
};

I added the useContext hook in <HeaderInfo>. Now the “loginState” is available in scope. I used destructuring to extract the array elements.

const [loginState, loginDispatch] = useContext(LoginContext);

Note that loginDispatch is not used in <HeaderInfo> so you can remove that.

Similarly, add useContext to other components:

const LoginForm = () => {
  const [, loginDispatch] = useContext(LoginContext);
  const [name, setName] = useState("");
  return (
    <>
      <NameForm name={name} setName={setName} />
      <button
        type="button"
        onClick={() => loginDispatch({ type: "LOGIN", userName: name })}
      >
        Login
      </button>
      <h4>You typed {name}</h4>
    </>
  );
};
const LogoutForm = () => {
  const [, loginDispatch] = useContext(LoginContext);
  return (
    <button type="button" onClick={() => loginDispatch({ type: "LOGOUT" })}>
      Logout
    </button>
  );
};

The loginState and loginDispatch are back in scope, and usable exactly as they were before.

And voilà, our application is fully functional again.

To avoid distractions, the components in this example are all defined in one JavaScript file, but they don’t have to be. Each component can reside in its own file and the application will work just the same.

The complete version is available here.

Review it and feel free to experiment with it and make changes to see how it works.

There you have it, simple state management using React hooks. We managed state with useState, useReducer, useContext and the Context API.

I hope this was useful and fun. I’m glad you read my blog and looking forward to bringing you future posts.

References:

https://reactjs.org/docs/hooks-reference.html#usecontext

https://kentcdodds.com/blog/how-to-use-react-context-effectively