Jotai, Simplifying Global State

March 17, 2023


Jumbotron image by unsplash
Photos from Unsplash

Introduction

In my early stage of learning web development, i used React Redux to handle my state management. It made me believed that state management has to be complicated and has lots of boilerplate to share data across the entire application. When i explored React hooks, the idea of React context amazes me. It can be used on various kind and scope of the project, easy-to-maintain, and less complex than Redux.


Back then, it was an alternative to using state manager with what react itself has to offer. It was quite a game-changer — at least for me — . Main key advantages are:

  • Avoid Props-Drilling



    Before state manager was introduced, data sharing across the component was a nightmare. The idea of lifting state up is taken literally, where you have these data such as deciding dark mode or light mode, logged in user, routings, or localization things.


    We know that React is component-based approach, one component should be atomic and doing it’s own purpose. With this concept in mind, as app complexity grows, we can only imagine a prop is being passed through so many layers where it needs the data.


    However, in React Context we just need to wrap a component up as ProviderComponent with its respective children. Therefore, we can share data among the usual parent-child component relation or siblings component.

  • No extra libraries needed


    We can lift up state and make it accessible across the components using hooks. Mostly used are useContext, creating custom hooks, useCallbacks for solving re-rendering issues, and useMemo for expensive calculations.

It gives some sense of scalability without needing us to install another libraries — and achieving the same result — . Until we see something like this:


1// Wrappers everywhere! 
2function App() {
3  return (
4    <AuthProvider>
5      <UserProvider>
6        <RouteProvider>
7          <Routes />
8        </RouteProvider>
9      </UserProvider>
10    </AuthProvider>
11  )
12}


If we remember, the nature of react rendering strategy is as follows:


  • When the props passed from parent to child component has value changes, it re-renders
  • When the parent component re-renders, the child component re-renders as well

In nature, we can use useCallback() and useMemo() for handling unnecessary renderings. Turns out, that memoization process has its own cost. I’ve encountered Kent C Dodds’ findings related to this issue. Feel free to read his post if you will.


We will get to know Jotai. a state manager that simplifies data sharing across components, how it has smoother DX to me, and presumably has better performance handling than React Context.


Atoms Everywhere

When we want to lift up the state, we can use atoms that are sharable across the entire web component. we use atoms as the simplest form of state. This is contrast to Redux and Zustand, which use a huge state that consist of lots of smaller states as its attribute.

By having atoms, we don't need to stress about updating subscribers and reducer logics as the app scales.


Here is an example of how to make a state accessible from any component that it requires for both React Context and Jotai:


1// React Context Usage
2
3// CounterContext.js
4import React, { createContext, useContext, useState } from 'react';
5
6const CounterContext = createContext();
7
8export function useCounter() {
9  return useContext(CounterContext);
10}
11
12export function CounterProvider({ children }) {
13  const [count, setCount] = useState(0);
14
15  const increment = () => setCount(prevCount => prevCount + 1);
16  const decrement = () => setCount(prevCount => prevCount - 1);
17
18  const value = {
19    count,
20    increment,
21    decrement,
22  };
23
24  return (
25    <CounterContext.Provider value={value}>
26      {children}
27    </CounterContext.Provider>
28  );
29}
30    

1// Then, use it in the respective component
2// AnyComponent.js
3import React from 'react';
4import { useCounter } from './CounterContext';
5
6function AnyComponent() {
7  const { count, increment, decrement } = useCounter();
8
9  return (
10    <div>
11      <p>Count: {count}</p>
12      <button onClick={increment}>Increment</button>
13      <button onClick={decrement}>Decrement</button>
14    </div>
15  );
16}
17
18export default AnyComponent;
19

Now let's look at how we'd do it in Jotai:


1// Jotai usage
2
3// CounterAtom.js
4import { atom, useAtom } from 'jotai';
5
6export const counterAtom = atom(0);
7
8// anyComponent.js
9import counterAtom from 'path/to/atoms/CounterAtom.js'
10
11// We can directly use it with special hooks from jotai. 
12// More on Getters and Setters (next section)!
13export function AnyComponent {
14  const [count, setCount] = useAtom(counterAtom)
15  // ... value and setters ready to use
16}
17        

Basically Jotai atoms are written just like that. That's why i'd like to put all the atoms inside a file called store.js. For example, we could change the wrapper hell in react context as follows:


1// store.js
2
3// The value inside atom() is atom's default value
4export const authAtom = atom({})
5export const userAtom = atom({})
6export const routeAtom = atom({})
7

Getters and Setters

There are multiple ways for us to access and manipulate the atom content.


  • useAtomValue() — When we need to access atom’s value only in a component
  • useSetAtom() — When we need to update atom’s value in a component
  • useAtom() — When we need both access and updating atom’s value in a component

You might notice that getters and setters are available in different hooks, this solves the extra re-render issue of React context, and eliminates the need for any memoization.


Providers are optional

Jotai atoms serves the global state by default. Moreover, with its simple syntax, i’d like to put all the atoms inside a file called store.js.


Let’s take a look at wrappers concern in React Context


1// Wrappers everywhere! 
2function App() {
3  return (
4    <AuthProvider>
5      <UserProvider>
6        <RouteProvider>
7          <Routes />
8        </RouteProvider>
9      </UserProvider>
10    </AuthProvider>
11  )
12}

In Jotai, those wrappers can be replaced as follows:


1// store.js
2
3// The value inside atom() is atom's default value
4export const authAtom = atom({})
5export const userAtom = atom({})
6export const routeAtom = atom({})
7

We only use providers when we want to achieve one of these things:


  • To provide a different state for each sub tree.
  • To accept initial values of atoms.
  • To clear all atoms by remounting.

Conclusion

In summary, consider using Jotai over React Context if you value simplicity, performance optimization, fine-grained reactivity, and predictable updates. React Context can be suitable for smaller applications or cases where complex state management is not a primary concern. However, for more advanced state management needs, alternatives like Jotai or Redux might provide more robust solutions.


©️ Muhammad Ilham Adhim - 2024