Jotai Atoms Are Just Functions

Bookmark
Slides

Jotai is a state management library. We have been developing it primarily for React, but it's conceptually not tied to React. It this talk, we will see how Jotai atoms work and learn about the mental model we should have. Atoms are framework-agnostic abstraction to represent states, and they are basically just functions. Understanding the atom abstraction will help designing and implementing states in your applications with Jotai

by



Transcription


Hello everyone. Thanks for an opportunity to give this talk. I hope you find it useful. As many of you may know, state management in react is one of the most discussed topics in the community. There are many libraries and solutions. UseStateHook is one of the primitive solutions. Some of the popular libraries include Redux, Movex, XState, and Zustand. They provide different functionalities for different goals. The good thing is that developers have many options to develop their apps. The bad thing is that there are too many options. But I think having many options is still good for ecosystem. If there were only one solution, we would miss many new ideas. Jotai is a new library in this field. Hi, my name is Daishikato. I'm author of Jotai library. I am half open source developer and half freelancer. My open source software is with javascript and react. And my work is also related with javascript and react. There are quite a few open source projects that I'm working on, including experimental ones. Jotai is one of my open source projects, but we develop it as a team. While I'm the main developer of the code, there are many contributors not only for coding, but also for documentation and other stuff. This talk is about Jotai library, which is one solution for state management in react. Jotai is a library based on atoms, which represent pieces of state. Atoms are popularized by a library called Recoil, but the concept is not very new. The concept is basically to form a dependency graph of pieces of state and propagate updates. For example, suppose we have three atoms, A, B, and C. A depends on B, B depends on C. If we update C, both B and A are updated. This pattern is already done, for example, with observables for async data flow. Atoms are a little different from observables. Usually, an observable object would hold a value or maybe it's initially empty. Atoms would never hold values. They are just definitions and values exist somewhere else. We will get into it in this talk. Let's first see how the usage of Jotai looks like. This is a simple example using Jotai atoms. We have three atoms, textAtom, textLengthAtom, and uppercase Atom. textAtom has an initial value, hello. textLengthAtom has a function returning the length of textAtom. uppercaseAtom has a function similarly returning the uppercase string of textAtom. textLengthAtom and uppercaseAtom both depend on textAtom. So if you change textAtom, the other two atoms will also be changed. As you see, if we enter a text in the text field, all three values are updated accordingly. If you look closely, we use useAtom hook which takes an atom we defined. useAtom hook works like useState hook. It returns a value and an update function. If the value is changed, it will trigger a re-render. You can change the value with the update function. There's one important note which isn't shown in this example. If we use the same atom, useAtom returns the same value. So we can use atoms for global state. Jotai library is often considered as a global state solution. We can use it for global state, but it's not truly global. And we can use it for semi-global or local states. This may sound unintuitive, but if you think atoms as functions, it should make more sense. Let's try to make an analogy. We all know react components are just functions. This is one of the simplest components. It returns a string. We usually define components that return JSX elements, but returning strings is also valid. We don't exactly know when this function is invoked. react will invoke this function whenever necessary. It's declarative. Components can be said to be declarative functions. Let's move on. Components are functions. Likewise, atoms are functions. This is a similar example, and it's a varied atom definition. We have a wrapping function named atom, but it's not very important. It's just a helpful function to build an atom config object, which helps especially in typescript. In this case, it will create an object with the read property, which contains the function. The function is exactly the same as what we saw in the text component in the previous slide. This is an atom whose value is the return value of the function, which is a string, hello. It's so to say a static atom whose value never changes. A static atom is not very useful, so how do we add a dependency? Let's see how a component looks like. With react components, there are roughly three patterns to define dependencies, props, states, and context. Let's take context as an example because it's good for analogy. In this example, we create count context and define double component. Double component will use the context value, double it, and return it. Whenever the context value is changed, we expect double component will return an updated value. But we don't know when the component function will be invoked because it's declarative. What we declare is that double component depends on count context. How the function is invoked is not determined at this point. Are you okay with this example? So how does an atom look like? Here's a simple example with atoms. We define count atom and double atom. The definition of the count atom is a special syntax which corresponds to create context in the previous example. We will learn about this syntax a little bit later. So far, it's an atom definition that we can use from other atoms. The following is double atom definition. Notice a function named get. We don't define it. It will be injected from somewhere. The get function will return the value of an atom. It's like use context returns the context value. Double atom depends on count atom. And when count atom changes, double atom should be re-evaluated. When we define atoms, we don't know what are atom values or even where atom values are stored. What we know is when the function is invoked, it will receive the get function and using the get function, you can get the value of the atom. This is pretty similar to how use context hook works. The difference is that in atoms, the get function is injected from the function parameter, whereas use context is exported from the react library. There's another difference. We can change atom dependencies. We define a new atom called quadruple atom. The internal function returns double value of double atom, which is the quadruple value of the original count atom. Quadruple atom depends on double atom and double atom depends on count atom. So the dependencies are chained. In practice, it will form a tree or even graph of dependencies. Now we learn the basic pattern of how atoms form a simple dependency graph. Some more patterns are possible. For example, single atom can depend on multiple atoms. Another example is atom dependency can be conditional. Using those patterns, we can freely define atoms and make a complex dependency graph. We basically covered how to define atoms in terms of reading their values. Let's see how we can update the values. We change the definition of double atom by adding a second parameter. The first parameter is called read function and the second parameter is called write function. Those functions are just put in the read and write properties of the resulting atom object. The write function has three parameters, get, set, and new value. In this case, it divides a new value by two and set it to the original count atom. Providing the write function makes the atom writable. The read and write functions of double atom are symmetric, but they don't have to be symmetric. We can define those functions freely. This opens up a pattern called write only atoms, but it's out of the scope of this talk. Now the final missing piece is the definition of count atom at the first line. We call such atoms primitive atoms. From the perspective of the dependency graph, they are the sources of data. Primitive atoms are defined by the atom function with a single initial value. In this case of this count atom, zero is the initial value. This is a special syntax to create a writable atom with an initial value. The resulting atom config object has three properties, init, read, and write. The init property has the initial value. The read and write functions do simply read from and write to the self atom. We have learned a normal read only atom, a writable atom, and a primitive atom. Read only atoms are atoms that compute from other atoms. Writable atom are read only atoms plus write back capability. Primitive atoms are special writable atoms that can write to themselves. They form a dependency graph and primitive atoms are the source of the graph. At the beginning of this talk, I introduced Jotai as a state management solution for react. As we saw how to build atoms, they have nothing to do with react. Atoms are pure definitions of state that depends only on javascript. The atom function may seem like magic, but it's just a helper function to make a config object. Technically, you could define the atom object without using the helper function. So if atoms are not things of react, how can we use them with react? To use atoms in react, we need two functions. One is provider component. The other is useAtomHook. As I keep saying, atom objects are just definitions and they don't hold values. So we need a place to store atom values. That's the provider component. It has values of all atoms under the component tree. To use an atom value in the provider component, we use useAtomHook in child components. It takes an atom object and returns the current value in the provider component and the update function to change the value in provider. UseAtom's usage is similar to useState and useReducer. So learning useAtom is almost nothing for developers who are already familiar with useState. Let's look at the same example we looked at earlier again. textAtom is a primitive atom and the other two atoms are read-only atoms. They depend on text atoms. So when textAtom value changes, all three atom values change and updates can be seen on screen. The code in the slide doesn't show the provider component, but it's there at the bottom. However, in some cases, we can actually omit provider. For many use cases, we would have one provider close to the root of component tree. If it is just one provider in the memory, we can omit it. It then uses a global state for atom values. It's a very handy feature and often used. If we never have a provider component, theoretically, atoms can hold values. So what's the point of separating atoms and values? I just said if there is one provider in the memory, we can omit it. That means for other cases, having multiple providers is meaningful and it's possible if atoms don't hold values. This is an example with two providers. Notice we have one atom defined, which is countAtom. The counter component is also defined once. Now we use the counter component in two providers. Because those two providers have different atom values, we see two different values on screen. Let's see how it works. The counts are separated. Clicking buttons affects only the count in one provider. This is what I mean. Atom definition is reusable. It's just like a function. There's another case where atoms shouldn't hold values. In server context, there are multiple requests and each request starts a new render. We need a provider component for each render to isolate states between requests. Atom definitions can be shared between requests, but atom values are not. This is actually hypothetical because at the moment, there's basically no way to update atom values on server. That being said, it feels nice to separate definition and values if we handle multiple things in a single memory space. And we could do more things on server in the future. So if atoms are just definitions and independent from react, can we use them without react? To answer that question, I made an experimental library called Jyotai.jsx. It's a library that technically replaces react and react DOM libraries. The syntax is exactly the same as normal Jyotai usage with react. We can run a Jyotai app with only changing the import lines. It is not feature complete and may contain bugs, but it shows a proof of concept. Other two experimental libraries are called Jyotai.signal and Jyotai.uncontrolled. They basically work with react, but the syntax is different. The one is a signal style and the other is for uncontrolled components. Their implementations come with hacks, but they should also show the proof of atom concept. In the future, the Jyotai library will eventually expose vanilla functions that are not tied to react, and we would be able to build more libraries in our ecosystem. These three libraries are hosted under the GitHub organization named Jyotai Labs. There, we hope to improve libraries for practical use and add more libraries for experiments. In summary, the takeaway is the following. Jyotai atoms are just like functions, and they are framework agnostic state definitions. If you are already Jyotai users, please try to keep that in mind, which may help designing atoms. If you are new to Jyotai library, please give it a try. We have a website at jyotai.org, and there are some documentations. We look forward to communicating with users and contributors. This is the end of my talk. Thank you.
22 min
05 Dec, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic