Jotai React - Better Global State Management
Jotai React - Better Global State Management

In this post you will learn such library which is called Jotai and it can help you tremendously with state management inside React.

If we are talking about React typically you will use useState inside your components. This is totally fine but this is just a local state.

const UsersTable = () => {
  const [users, setUsers] = useState([])
}

This is just a local state. You can't share this state between the components.

Actually you can by using parent and child communication but it is not that efficient if your components are on a different levels of nesting or you want to share your data between several components.

Which actually means that you want some global state. In this case you will typically use React.context, you throw your state inside it and you have it everywhere.

export const UsersContext = createContext()

export const UsersProvider = ({children}) => {
  const value = useState(initialState)

  return <UsersContext.Provider value={value}>{children}</QuizContext.Provider>
}

But the main problem is that all your components that are subscribed to this context will be rerendered with every change of any property inside the state. Then you start to create more and more contexts you get even more rerenders and it is not that easy to support.

At this moment you might think "Maybe I will just take Redux and implement with it one single global store". This is totally fine, you can do that but with Redux you will write lots of code.

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    addUser(state) {
      state.users.push(state.username);
      state.username = "";
    },
    changeUsername(state, action) {
      state.username = action.payload;
    },
    changeSearch(state, action) {
      state.search = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.users = action.payload;
      });
  },
});

Redux is a big but super scalable solution that you might want to use for some projects.

But there is another way. How can we share our local state without using Redux and without creating custom providers?

Jotai website

This is the official website of the library which is called Jotai. The main idea is that it is super simple. This is just a replacement of useState.

const countAtom = atom(0)

First of all we create an atom like a state inside React.

const Counter = () => {
  const [count, setCount] = useAtom(countAtom)
}

Then in any component we can get and set our state by using useAtom. And as you can see the code is super similar to useState.

You can use useAtom inside any component and get the same state.

You don't need to implement React.Context, you don't need to manage your providers and you don't need to think about rerendering too much.

Real example

Initial project

Here I already have a small application where we render a list of users. And we have 2 different routes Users and Popular Users.

The main idea is that we want to try and implement in our application Jotai.

For this we must first install Jotai.

npm i jotai

Our next step is to create an atom in some place were we can define state that we need. For example App component.

But first of all we need to discuss what kind of state do we need at all.

Essentially in your application you have 2 different types of states. You either have server state where you store fetched data from the API and synchronize this data with component state or you have some client state like themes or filters.

The main thing to remember is that Jotai implements the client state. Which actually means all properties that we create inside Javascript are client state.

This is why let's define a client state for our application which will be theme.

import { atom } from "jotai";
export const themeAtom = atom("light");

const App = () => {
  ...
}

Now we can reuse this themeAtom state across all components without any providers or passing this value from top to bottom.

Let's try to use this information inside Users component.

import { useAtom } from "jotai";
import { themeAtom } from "./App";

const Users = () => {
  ...
  const [theme] = useAtom(themeAtom);

  return (
    <div>
      ...
      <div>Our theme is {theme}</div>
    </div>
  );
};

With the help of useAtom function we can get access to our themeAtom and render information in component just like with useState.

Theme atom

As you can see we successfully rendered our theme in the component. This state is now shared across all components.

We can copy paste this logic to PopularUsers component and get the same behavior.

import { useAtom } from "jotai";
import { themeAtom } from "./App";

const PopularUsers = () => {
  ...
  const [theme] = useAtom(themeAtom);

  return (
    <div>
      ...
      <div>Our theme is {theme}</div>
    </div>
  );
};

As you can see in browser it works exactly the same.

Additionally at some point we need to change our state in Jotai. Let's create a button for that.

import { useAtom } from "jotai";
import { themeAtom } from "./App";

const Users = () => {
  ...
  const [theme, setTheme] = useAtom(themeAtom);
  const toggleTheme = () => {
    const newTheme = theme === "light" ? "dark" : "light";
    setTheme(newTheme);
  };

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
      <div>
        Our theme is {theme}
        <button onClick={toggleTheme}>
          Toggle theme
        </button>
      </div>
    </div>
  );
};

Here we added setTheme just like in useState and we change our state in Jotai by calling it.

Set jotai

As you can see our theme is changed to dark and all components which are subscribed to this atom are rerendered.

Atoms based on atoms

In some cases you want to create atoms which are based on the values of other atoms.

import { atom } from "jotai";

export const themeAtom = atom("light");
export const buttonColorAtom = atom((get) =>
  get(themeAtom) === "light" ? "gray" : "white"
);

Here in our App component we defined buttonColorAtom. Instead of providing a value inside we have a function. It has a parameter get which allows us to read values from other atoms. So we can change the value of buttonColorAtom based on our themeAtom.

Now let's try to use this buttonColorAtom.

import { buttonColorAtom, themeAtom } from "./App";

const Users = () => {
  const [theme, setTheme] = useAtom(themeAtom);
  const [buttonColor] = useAtom(buttonColorAtom);

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
      <div>
        Our theme is {theme}{" "}
        <button onClick={toggleTheme} style={{ background: buttonColor }}>
          Toggle theme
        </button>
      </div>
    </div>
  );
};

Just like before we got buttonColorAtom with useAtom and used it to provide styles in our button.

Color Atom

As you can see in browser our color is applied to the button when we have a dark theme.

The best use of atoms is to create small values without big global objects. Then Jotai can do it's best and avoid rerenders as often as possible. You don't want to have a single object and rerender your whole application when some value changed.

Async atoms

Additionally in Jotai we can create asynchronous atom. You can do that and it will work but this is not the usage of this library when it shines. As you already was in our application I'm fetching the list of users from the API. And actually for this I'm using an additional library which is called react-query.

React Query library is amazing to work with API, fetch data, cache data and invalidate cache. This is exactly the library which works only with server state.

Which means we fetch something from the API and we want to sync this data with the state inside components.

const Users = () => {
  const { data: users = [] } = useQuery(["users"], getUsers);
}

This is exactly what this library does. In a single line I'm fetching users data from the API and store it directly in the component state. What is more important this data is cached between different routes. This is why when we just between Users and PopularUsers we don't have additional fetch and our data is rendered from the cache.

This is why these 2 libraries really work nicely together. We use Jotai when we need a client state and global state and we use React Query when we want to work with API and synchronize API data with the component.

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done