Simple way to manage state in React with Context
Tags: react
Published: 2020-08-03 19:41:31

## What we'll learn

We will learn how to use React's context API to manage state. Also, we'll see how to use `useSWR` hook from [swr][swr-link] to manage async data from an API.

## Our Requirements

1. Data can come from synchronous or asynchronous calls. An API endpoint or a simple `setState`.
2. Allow to update state data from the components that use it.
3. No extra steps like actions, thunks.

## Small introduction to `swr`

`SWR (stale-while-revalidate)` is a caching strategy where data is returned from a cache immediately and send fetch request to server. Finally, when the server response is available, get the new data with changes from the server as well as updating the cache.

Here we are talking about the [swr][swr-link] library from [vercel][vercel-link]. It provides a hook `useSWR` which we will use to fetch data from GitHub API.

Head over to `swr`'s docs to learn more. The API is small and easy.

## Store

We need a top-level component where will maintain this global state. Let's call this component `GlobalStateComponent`. If you've used `Redux`, this can be your store.

We'll test with 2 types of data for better a understanding.

- Users data coming from an API like GitHub which might not change pretty quickly.
- A simple counter which increments count by 1 every second.

```jsx
// global-store.jsx

const GlobalStateContext = React.createContext({
  users: [],
  count: 0,
});

export function GlobalStateProvider(props) {
  // we'll update here
  return <GlobalStateContext.Provider value={value} {...props} />;
}

// a hook which we are going to use whenever we need data from `GlobalStateProvider`

export function useGlobalState() {
  const context = React.useContext(GlobalStateContext);

  if (!context) {
    throw new Error("You need to wrap GlobalStateProvider.");
  }

  return context;
}
```

Now we need to use `useSWR` hook to fetch users data. Basic API for `useSWR` looks like this.

```jsx
const { data, error, mutate } = useSWR("url", fetcher, [options]);

// url - an API endpoint url.
// fetcher - a function which takes the first argument as parameters (url here)
// and returns a promise.
// options - Options for the hook. Configuration for this hook.

// data - response from the API request
// error - Error response from fetcher will be caught here.
// mutate - Update the cache and get new data from server.
```

We will use browser's built-in fetch API. You can use Axios or any other library you prefer.

```jsx
const fetcher = (url) => fetch(url).then((res) => res.json());
```

With this, our complete `useSWR` hook looks like this.

```jsx
const { data, error, mutate } = useSWR(`https://api.github.com/users`, fetcher);
```

And, we need a `setState` with count and a `setInterval` which updates the count every second.

```jsx
...
const [count, setCount] = React.useState(0);
const interval = React.useRef();

React.useEffect(() => {
  interval.current = setInterval(() => {
        setCount(count => count + 1);
  }, 1000);

  return () => {
    interval.current && clearInterval(interval.current);
  }
}, []);
...
```

A context provider takes a `value` prop for the data. Our value will be both `user` related data and `count`.

If we put all these little things together in a `global-store.jsx` file, it looks like this.

```jsx
// global-store.jsx

const GlobalStateContext = React.createContext({
  users: [],
  mutateUsers: () => {},
  error: null,
  count: 0,
});

export function GlobalStateProvider(props) {
  const { data: users, error, mutate: mutateUsers } = useSWR(
    `https://api.github.com/users`,
    fetcher
  );
  const [count, setCount] = React.useState(0);
  const interval = React.useRef();

  React.useEffect(() => {
    interval.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);

    return () => {
      interval.current && clearInterval(interval.current);
    };
  }, []);

  const value = React.useMemo(() => ({ users, error, mutateUsers, count }), [
    users,
    error,
    mutateUsers,
    count,
  ]);

  return <GlobalStateContext.Provider value={value} {...props} />;
}

// a hook to use whenever we need to consume data from `GlobalStateProvider`.
// So, We don't need React.useContext everywhere we need data from GlobalStateContext.

export function useGlobalState() {
  const context = React.useContext(GlobalStateContext);

  if (!context) {
    throw new Error("You need to wrap GlobalStateProvider.");
  }

  return context;
}
```

## How to use it

Wrap your top-level component with `GlobalStateProvider`.

```jsx
// app.jsx
export default function App() {
  return <GlobalStateProvider>//...</GlobalStateProvider>;
}
```

Let's have two components, one consumes users data and another one needs counter.

We can use `useGlobalState` hook we created in both of them to get `users` and `count`.

```jsx
// users.jsx

export default function Users() {
  const { users, error } = useGlobalState();

  if (!users && !error) {
    return <div>Loading...</div>;
  }

  return <ul>...use `users` here</ul>;
}
```

```jsx
// counter.jsx

export default function Counter() {
  const { count } = useGlobalState();

  return <div>Count: {count}</div>;
}
```

```jsx
// app.jsx

export default function App() {
  return (
    <GlobalStateProvider>
      <Counter />
      <Users />
    </GlobalStateProvider>
  );
}
```

That's it. Now you'll see both Counter and Users.

The codesandox link: [codesandbox][react-context-global-state]
{% codesandbox react-context-global-state-2ih3u %}

## But, Wait

If you put a `console.log` in both `Users` and `Counter` components, you'll see even if only `count` updated, `Users` component also renders.

The fix is simple. Extract `users` in a component between `App` and `Users`, and pass `users` as a prop to `Users` component, and wrap `Users` with `React.memo`.

```diff
// app.jsx

export default function App() {
  return (
    <GlobalStateProvider>
      <Counter />
-     <Users />
+     <UserWrapper />
    </GlobalStateProvider>
  )
}
```

```jsx
// user-wrapper.jsx

export default function UserWrapper() {
  const { users, error } = useGlobalState();
  return <Users users={users} error={error} />;
}
```

```diff
// users.jsx

- export default function Users() {
+ const Users = React.memo(function Users({users, error}) {
- const {users, error} = useGlobalState();

  if (!users && !error) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
        ...use users here
    </ul>
  )
});

export default Users;

```

Now check the `console.log` again. You should see only `Counter` component rendered.

The finished codesandbox link: [codesandbox][react-context-global-state-memo]

{% codesandbox react-context-global-state-memo-qs2is %}

## How to force-update users

Our second requirement was to update the state from any component.

In the same above code, if we pass `setCounter` and `mutateUsers` in the context provider's `value` prop, you can use those functions to update the state.

`setCounter` will update the counter and `mutateUsers` will resend the API request and returns new data.

You can use this method to maintain any synchronous, asynchronous data without third-party state management libraries.

## Closing Notes

- Consider using `useReducer` instead of `useState` if you end up with too many `setState`s in global state. A good use case will be if you are storing a large object instead of a single value like `count` above. Splitting up that object in multiple `setState` means any change in each of them will re-render all the components using your context provider. It'll get annyoing to keep track and bring in `React.memo` for every little thing.
- [react-query][react-query-link] is another solid library as an alternative to `swr`.
- Redux is still doing great for state management. The new
  [redux-toolkit][redux-toolkit-link] amazingly simplifies Redux usage. Check it out.
- Have an eye on [recoil][recoil-link], A new state management library with easy sync, async state support. I didn't use it on a project yet. I'll definitely try it soon.

Thank you and have a great day. 😀 👋

[swr-link]: https://swr.vercel.app/
[react-query-link]: https://github.com/tannerlinsley/react-query
[vercel-link]: https://vercel.com/
[recoil-link]: https://recoiljs.org/
[redux-toolkit-link]: https://redux-toolkit.js.org/
[react-context-global-state]: https://codesandbox.io/s/react-context-global-state-2ih3u
[react-context-global-state-memo]: https://codesandbox.io/s/react-context-global-state-memo-qs2is