Technical Documentation: pmndrs/jotai
βΉοΈ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference
Overview (`README.md`)
visit jotai.org or npm i jotai
Jotai scales from a simple useState replacement to an enterprise TypeScript application.
- Minimal core API (2kb)
- Many utilities and extensions
- No string keys (compared to Recoil)
First, create a primitive atom
An atom represents a piece of state. All you need is to specify an initial
value, which can be primitive values like strings and numbers, objects, and
arrays. You can create as many primitive atoms as you want.
import { atom } from 'jotai'
const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 })Use the atom in your components
It can be used like React.useState:
import { useAtom } from 'jotai'
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<h1>
{count}
<button onClick={() => setCount((c) => c + 1)}>one up</button>
...Create derived atoms with computed values
A new read-only atom can be created from existing atoms by passing a read
function as the first argument. get allows you to fetch the contextual value
of any atom.
const doubledCountAtom = atom((get) => get(countAtom) * 2)
function DoubleCounter() {
const [doubledCount] = useAtom(doubledCountAtom)
return <h2>{doubledCount}</h2>
}Creating an atom from multiple atoms
You can combine multiple atoms to create a derived atom.
const count1 = atom(1)
const count2 = atom(2)
const count3 = atom(3)
const sum = atom((get) => get(count1) + get(count2) + get(count3))Or if you like fp patterns ...
const atoms = [count1, count2, count3, ...otherAtoms]
const sum = atom((get) => atoms.map(get).reduce((acc, count) => acc + count))Derived async atoms [](https://react.dev/reference/react/Suspense)
You can make the read function an async function too.
const urlAtom = atom('https://json.host.com')
const fetchUrlAtom = atom(async (get) => {
const response = await fetch(get(urlAtom))
return await response.json()
})
function Status() {
// Re-renders the component after urlAtom is changed and the async function above concludes
const [json] = useAtom(fetchUrlAtom)
...You can create a writable derived atom
Specify a write function at the second argument. get will return the current
value of an atom. set will update the value of an atom.
const decrementCountAtom = atom(
(get) => get(countAtom),
(get, set, _arg) => set(countAtom, get(countAtom) - 1)
)
function Counter() {
const [count, decrement] = useAtom(decrementCountAtom)
return (
<h1>
{count}
<button onClick={decrement}>Decrease</button>
...Write only derived atoms
Just do not define a read function.
const multiplyCountAtom = atom(null, (get, set, by) =>
set(countAtom, get(countAtom) * by),
)
function Controls() {
const [, multiply] = useAtom(multiplyCountAtom)
return <button onClick={() => multiply(3)}>triple</button>
}Async actions
Just make the write function an async function and call set when you're ready.
const fetchCountAtom = atom(
(get) => get(countAtom),
async (_get, set, url) => {
const response = await fetch(url)
set(countAtom, (await response.json()).count)
}
)
function Controls() {
const [count, compute] = useAtom(fetchCountAtom)
return (
<button onClick={() => compute('http://count.host.com')}>compute</button>
...Note about functional programming
Jotai's fluid interface is no accident β atoms are monads, just like promises!
Monads are an established
pattern for modular, pure, robust and understandable code which is optimized for change.
Read more about Jotai and monads.
Links
Overview (`website/README.md`)
Getting Started
Install the dependencies:
pnpm install --ignore-workspaceMake a cache directory:
mkdir .cacheRun the development server:
pnpm run devOpen http://localhost:9000 with your browser to see the result.
Overview (`examples/hacker_news/README.md`)
Hacker News [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hacker_news)
Description
Browse Hacker News items with Jotai. Click the arrow button to load the next item.
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/hacker_news && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hacker_news
Overview (`examples/hello/README.md`)
Hello [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hello)
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/hello && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hello
Overview (`examples/mega-form/README.md`)
Mega form [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/mega-form)
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/mega-form && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/mega-form
Overview (`examples/starter/README.md`)
Starter [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/starter)
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/starter && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/starter
Overview (`examples/text_length/README.md`)
Text Length [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/text_length)
Description
Count the length and show the uppercase of any text.
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/text_length && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/text_length
Overview (`examples/todos/README.md`)
Todos [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos)
Description
Create a todo list, mark tasks as completed, and switch between Completed and Incomplete to filter items.
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/todos && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos
Overview (`examples/todos_with_atomFamily/README.md`)
Todos with atomFamily [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily)
β οΈ Note:
atomFamilyfromjotai/utilsis deprecated and will be removed in v3. For new projects, please use thejotai-familypackage instead.
Description
Implement a todo list using atomFamily and localStorage. You can save your todo list by clicking Save to localStorage, then restore it by clicking Load from localStorage.
Set up locally
git clone https://github.com/pmndrs/jotai
# install project dependencies & build the library
cd jotai && pnpm install
# move to the examples folder & install dependencies
cd examples/todos_with_atomFamily && pnpm install
# start the dev server
pnpm devSet up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily
Index (`docs/index.mdx`)
title: Documentation nav: 0
Welcome to the Jotai v2 documentation! Jotai's atomic approach to global React state management scales from a simple useState replacement to an enterprise application with complex requirements.
Features
- Minimal core API (2kb)
- Many utilities and extensions
- TypeScript oriented
- Works with Next.js, Waku, Remix, and React Native
Core
Jotai has a very minimal API, exposing only a few exports from the main jotai bundle. They are split into four categories below.
Utilities
Jotai also includes a jotai/utils bundle with a variety of extra utility functions. One example is atomWithStorage, which includes localStorage persistence and cross browser tab synchronization.
Extensions
Jotai has many officially maintained extensions including atomWithQuery for React Query and atomWithMachine for XState, among many others.
Third-party
Beyond the official extensions, there are many third-party community packages as well.
Tools
Use SWC and Babel compiler plugins for React Fast Refresh support and debugging labels. This creates the best developer experience when using a React framework such as Next.js or Waku.
Basics
Learn the basic concepts of the library, discover how it compares with others, and see usage examples.
Guides
Guides can help with use common cases such as TypeScript, React frameworks, and basic patterns.
Recipes
Recipes can help with more advanced patterns.
Basics Comparison (`docs/basics/comparison.mdx`)
title: Comparison nav: 7.02
Jotai was born to solve extra re-render issues in React.
An extra re-render is when the render process produces the same UI result,
where users won't see any differences.
To tackle this issue with React context (useContext + useState),
one would require many contexts and face some issues.
- Provider hell: It's likely that your root component has many context providers, which is technically okay, and sometimes desirable to provide context in different subtree.
- Dynamic addition/deletion: Adding a new context at runtime is not very nice, because you need to add a new provider and its children will be re-mounted.
Traditionally, a top-down solution to this is to use a selector function.
The use-context-selector library is one example.
The issue with this approach is the selector function needs to return
referentially equal values to prevent re-renders, and this often requires a memoization technique.
Jotai takes a bottom-up approach with the atomic model, inspired by Recoil.
One can build state combining atoms, and optimize renders based on atom dependency.
This avoids the need for memoization.
Jotai has two principles.
- Primitive: Its basic API is simple, like
useState. - Flexible: Atoms can derive another atom and form a graph. Atoms can also be updated by another arbitrary atom. It allows abstracting complex state models.
How is Jotai different from useContext of React?
Jotai's core API is minimalistic and makes it easy to build utilities based upon it.
Analogy
You can view Jotai as a drop-in replacement to useContext. Except Jotai is aiming for simplicity, minimalistic API and can do much more than useContext & useState.
Usage difference
We can see how we used to share data to children, compared to how we do it with Jotai. But let's use a real-world example where we have multiple Context in our app.
// 1. useState local state
const Component = () => {
const [state, setState] = useState(0)
}
// 2. Lift local state up and share it via context
const StateContext = createContext()
const Parent = ({ children }) => {
return (
<StateContext.Provider value={useState(0)}>
{children}
</StateContext.Provider>
)
}
const Component = () => {
const [state, setState] = useContext(StateContext)
}
// 3. Have multiple states and contexts
const State1Context = createContext()
const State2Context = createContext()
const Parent = ({ children }) => (
<State1Context.Provider value={useState(0)}>
<State2Context.Provider value={useState(0)}>
{children}
</State2Context.Provider>
</State1Context.Provider>
)
const Component1 = () => {
const [state, setState] = useContext(State1Context)
}
const Component2 = () => {
const [state, setState] = useContext(State2Context)
}Now let's see how Jotai simplify it for us. You can just use atoms instead of multiple Context.
import { Provider, atom, useAtom } from 'jotai'
const atom1 = atom(0)
const atom2 = atom(0)
// Optional: you can use Provider's just like useContext,
// ...but if you only need one,
// ...you can just omit it and Jotai will use a default one (called Provider-less mode).
const Parent = ({ children }) => {
return <Provider>{children}</Provider>
}
const Component1 = () => {
const [state, setState] = useAtom(atom1)
}
const Component2 = () => {
const [state, setState] = useAtom(atom2)
}How is Jotai different from Zustand?
Name
Jotai means "state" in Japanese.
Zustand means "state" in German.
Analogy
Jotai is like Recoil.
Zustand is like Redux.
Where state resides
To hold states,
Both have stores that can exist either at module level or at context level.
Jotai is designed to be context first, and module second.
Zustand is designed to be module first, and context second.
How to structure state
Jotai state consists of atoms (i.e. bottom-up).
Zustand state is one object (i.e. top-down).
Technical difference
The major difference is the state model. Zustand is a single store (although you could create multiple separate stores), while Jotai consists of primitive atoms and allows composing them together. In this sense, it's the matter of programming mental model.
When to use which
- If you need a replacement for useState+useContext, Jotai fits well.
- If you want a simple module state, Zustand fits well.
- If code splitting is important, Jotai should perform well.
- If you prefer Redux devtools, Zustand is good to go.
- If you want to make use of Suspense, Jotai is the one.
How is Jotai different from Recoil?
(Disclaimer: the author is not very familiar with Recoil, this may be biased and inaccurate.)
Developer
- Jotai is developed with collective work by a few developers in Poimandres (formerly react-spring) org.
- Recoil is developed by a team at Facebook.
Basis
- Jotai is focusing on primitive APIs for easy learning, and it's unopinionated. (The same philosophy with Zustand)
- Recoil is all-in-one, and it has various cache strategies.
Technical difference
- Jotai depends on atom object referential identities.
- Recoil depends on atom string keys.
When to use which
- If you want to learn something new, either should work.
- If you like Zustand, Jotai would also be pleasant.
- If you need React Context alternatives, Jotai comes with enough features.
- If you need to read and write atoms outside React, Jotai provides store API.
- If you would try to create a new library, Jotai might give good primitives.
- Otherwise, both are pretty similar about the general goals and basic techniques, so please try both and share your feedback with us.
Basics Concepts (`docs/basics/concepts.mdx`)
title: Concepts nav: 7.01
Jotai is a library that will make you return to the basics of React development & keep everything simple.
From scratch
Before trying to compare Jotai with what we may have known previously, let's just dive straight into something very simple.
The React world is very much like our world, it's a big set of small entities, we call them components, and we know that they have their own state. Structuring your components to interact altogether will create your app.
Now, the Jotai world also has its small entities, atoms, and they also have their state. Composing atoms will create your app state!
Jotai considers anything to be an atom, so you may say: Huh, I need objects and arrays, filter them and then sort them out.
And here's the beauty of it, Jotai gracefully lets you create dumb atoms derived from even more dumb atoms.
If, for example, I have a page with 2 tabs: online friends and offline friends.
I will have 2 atoms simply derived from a common, dumber source.
const dumbAtom = atom([{ name: 'Friend 1', online: false }])
const onlineAtom = atom((get) => get(dumbAtom).filter((item) => item.online))
const offlineAtom = atom((get) => get(dumbAtom).filter((item) => !item.online))And you could keep going on complexity forever.
Another incredible feature of Jotai is the built-in ability to suspend when using asynchronous atoms. This is a relatively new feature that needs more experimentation, but is definitely the future of how we will build React apps. Check out the docs for more info.
Basics Functional Programming And Jotai (`docs/basics/functional-programming-and-jotai.mdx`)
title: Functional programming and Jotai nav: 7.04
Unexpected similarities
If you look at getter functions long enough, you may see a striking resemblance
to a certain JavaScript language feature.
const nameAtom = atom('Visitor')
const countAtom = atom(1)
const greetingAtom = atom((get) => {
const name = get(nameAtom)
const count = get(countAtom)
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})Compare that code with asyncβawait:
const namePromise = Promise.resolve('Visitor')
const countPromise = Promise.resolve(1)
const greetingPromise = (async function () {
const name = await namePromise
const count = await countPromise
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})()This similarity is no coincidence. Both atoms and promises are Monadsβ , a
concept from functional programming. The syntax used in both greetingAtom andgreetingPromise is known as do-notation, a syntax sugar for the plainer
monad interface.
About monads
The monad interface is responsible for the fluidity of the atom and promise
interfaces. The monad interface allowed us to define greetingAtom in terms ofnameAtom and countAtom, and allowed us to define greetingPromise in terms
of namePromise and countPromise.
If you're curious, a structure (like Atom or Promise) is a monad if you can
implement the following functions for it. A fun exercise is trying to implementof, map and join for Arrays.
type SomeMonad<T> = /* for example... */ Array<T>
declare function of<T>(plainValue: T): SomeMonad<T>
declare function map<T, V>(
anInstance: SomeMonad<T>,
transformContents: (contents: T) => V,
): SomeMonad<V>
declare function join<T>(nestedInstances: SomeMonad<SomeMonad<T>>): SomeMonad<T>The shared heritage of Promises and Atoms means many patterns and best-practices
can be reused between them. Let's take a look at one.
Sequencing
When talking about callback hell, we often mention the boilerplate, the
indentation and the easy-to-miss mistakes. However, plumbing a single async
operation into another single async operation was not the end of the callback
struggle. What if we made four network calls and needed to wait for them all?
A snippet like this was common:
const nPending = 4
const results: string[]
function callback(err, data) {
if (err) throw err
results.push(data)
if (results.length === nPending) {
// do something with results...
}
}But what if the results have different types? and the order was important? Well,
we'd have a lot more frustrating work to do! This logic would be duplicated at
each usage, and would be easy to mess up. Since ES6, we simply call Promise.all:
declare function promiseAll<T>(promises: Array<Promise<T>>): Promise<Array<T>>Promise.all "rearranges" Array and Promise. It turns out this concept,
sequencing, can be implemented for all monadβTraversable pairs. Many kinds
of collections are Traversables, including Arrays. For example, this is a case
of sequencing specialized for atoms and arrays:
function sequenceAtomArray<T>(atoms: Array<Atom<T>>): Atom<Array<T>> {
return atom((get) => atoms.map(get))
}Culmination
Monads have been an interest to mathematicians for 60 years, and to programmers
for 40. There are many resources out there on patterns for monads. Take a look
at them! Here are a select few:
- Inventing Monads by Stepan Parunashvili
- How Monads Solve Problems by ThatsNoMoon
- Wiki page list of monad tutorials
- Typeclassopedia (for the curious)
Learning a neat trick on using promises may well translate to atoms, asPromise.all and sequenceAtomArray did. Monads are not magic, just unusually
useful, and a tool worth knowing.
Notes
[β ] The ES6 Promise is not a completely valid monad because it cannot nest other
Promises, e.g. Promise> is semantically equivalent toPromise<number>. This is why Promises only have a .then, and not both a.map and .flatMap. ES6 Promises are probably more properly described as
"monadic" rather than as monads.
Unlike ES6 Promises, the ES6 Array is a completely lawful monad.
Basics Showcase (`docs/basics/showcase.mdx`)
title: Showcase nav: 7.03
-
Count the length and show the uppercase of any text.
-
Demonstrate a news article with Jotai, hit next to see more articles.
-
Record your todo list by typing them into this app, check them off if you have completed the task, and switch between
CompletedandIncompletedto see the status of your task. Todos example with atomFamily and localStorage
Implement a todo list using atomFamily and localStorage. You can store your todo list to localStorage by clicking
Save to localStorage, then remove your todo list and try restoring them by clickingLoad from localStorage.-
An UTC time electronic clock implementation using Next.js and Jotai.
-
A game of tic tac toe implemented with Jotai.
-
A simple demo to use Jotai with react-three-fiber
Core Atom (`docs/core/atom.mdx`)
title: atom nav: 2.01 keywords: atom,primitive,derived,debug,label,onmount
atom
The atom function is to create an atom config.
We call it "atom config" as it's just a definition and it doesn't yet hold a value.
We may also call it just "atom" if the context is clear.
An atom config is an immutable object. The atom config object doesn't hold a value. The atom value exists in a store.
To create a primitive atom (config), all you need is to provide an initial value.
import { atom } from 'jotai'
const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })You can also create derived atoms. We have three patterns:
- Read-only atom
- Write-only atom
- Read-Write atom
To create derived atoms, we pass a read function and an optional write function.
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set, update) => {
// `update` is any single value we receive for updating this atom
set(priceAtom, get(priceAtom) - update.discount)
// or we can pass a function as the second parameter
// the function will be invoked,
// receiving the atom's current value as its first parameter
set(priceAtom, (price) => price - update.discount)
},
)
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
// you can set as many atoms as you want at the same time
},
)get in the read function is to read the atom value.
It's reactive and read dependencies are tracked.
get in the write function is also to read atom value, but it's not tracked.
Furthermore, it can't read unresolved async values in Jotai v1 API.
set in the write function is to write atom value.
It will invoke the write function of the target atom.
Note about creating an atom in render function
Atom configs can be created anywhere, but referential equality is important.
They can be created dynamically too.
To create an atom in render function, useMemo or useRef is required to get a stable reference. If in doubt about using useMemo or useRef for memoization, use useMemo.
Otherwise, it can cause infinite loop with useAtom.
const Component = ({ value }) => {
const valueAtom = useMemo(() => atom({ value }), [value])
// ...
}Signatures
// primitive atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>
// read-only atom
function atom<Value>(read: (get: Getter) => Value): Atom<Value>
// writable derived atom
function atom<Value, Args extends unknown[], Result>(
read: (get: Getter) => Value,
write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>
// write-only derived atom
function atom<Value, Args extends unknown[], Result>(
read: Value,
write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>initialValue: the initial value that the atom will return until its value is changed.read: a function that's evaluated whenever the atom is read. The signature ofreadis(get) => Value, andgetis a function that takes an atom config and returns its value stored in Provider as described below. Dependency is tracked, so ifgetis used for an atom at least once, thereadwill be reevaluated whenever the atom value is changed.write: a function mostly used for mutating atom's values, for a better description; it gets called whenever we call the second value of the returned pair ofuseAtom, theuseAtom()[1]. The default value of this function in the primitive atom will change the value of that atom. The signature ofwriteis(get, set, ...args) => Result.getis similar to the one described above, but it doesn't track the dependency.setis a function that takes an atom config and a new value which then updates the atom value in Provider....argsis the arguments that we receive when we calluseAtom()[1].Resultis the return value of thewritefunction.
const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)There are two kinds of atoms: a writable atom and a read-only atom. Primitive atoms are always writable. Derived atoms are writable if the write is specified. The write of primitive atoms is equivalent to the setState of React.useState.
`debugLabel` property
The created atom config can have an optional property debugLabel. The debug label is used to display the atom in debugging. See Debugging guide for more information.
Note: While, the debug labels donβt have to be unique, itβs generally recommended to make them distinguishable.
`onMount` property
The created atom config can have an optional property onMount. onMount is a function which takes a function setAtom and returns onUnmount function optionally.
The onMount function is called when the atom is subscribed in the provider in the first time,
and onUnmount is called when itβs no longer subscribed.
In some cases (like React strict mode),
an atom can be unmounted and then mounted immediately.
const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
console.log('atom is mounted in provider')
setAtom(c => c + 1) // increment count on mount
return () => { ... } // return optional onUnmount function
}
const Component = () => {
// `onMount` will be called when the component is mounted in the following cases:
useAtom(anAtom)
useAtomValue(anAtom)
// however, in the following cases,
// `onMount` will not be called because the atom is not subscribed:
useSetAtom(anAtom)
useAtomCallback(
useCallback((get) => get(anAtom), []),
)
// ...
}Calling setAtom function will invoke the atomβs write. Customizing write allows changing the behavior.
const countAtom = atom(1)
const derivedAtom = atom(
(get) => get(countAtom),
(get, set, action) => {
if (action.type === 'init') {
set(countAtom, 10)
} else if (action.type === 'inc') {
set(countAtom, (c) => c + 1)
}
},
)
derivedAtom.onMount = (setAtom) => {
setAtom({ type: 'init' })
}Advanced API
Since Jotai v2, the read function has the second argument options.
`options.signal`
It uses AbortController so that you can abort async functions.
Abort is triggered before new calculation (invoking read function) is started.
How to use it:
const readOnlyDerivedAtom = atom(async (get, { signal }) => {
// use signal to abort your function
})
const writableDerivedAtom = atom(
async (get, { signal }) => {
// use signal to abort your function
},
(get, set, arg) => {
// ...
},
)The signal value is AbortSignal.
You can check signal.aborted boolean value, or use abort event with addEventListener.
For fetch use case, we can simply pass signal.
See the below example for fetch usage.
`options.setSelf`
β οΈ This internal feature was only available in Jotai v2 and has been removed in v3.
Stackblitz
import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'
const userIdAtom = atom(1)
const userAtom = atom(async (get, { signal }) => {
const userId = get(userIdAtom)
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`,
{ signal },
)
return response.json()
})
const Controls = () => {
const [userId, setUserId] = useAtom(userIdAtom)
return (
<div>
User Id: {userId}
<button onClick={() => setUserId((c) => c - 1)}>Prev</button>
<button onClick={() => setUserId((c) => c + 1)}>Next</button>
</div>
)
}
const UserName = () => {
const [user] = useAtom(userAtom)
return <div>User name: {user.name}</div>
}
const App = () => (
<>
<Controls />
<Suspense fallback="Loading...">
<UserName />
</Suspense>
</>
)
export default AppCore Provider (`docs/core/provider.mdx`)
title: Provider nav: 2.04 keywords: provider,usestore,ssr
Provider
The Provider component is to provide state for a component sub tree.
Multiple Providers can be used for multiple subtrees, and they can even be nested.
This works just like React Context.
If an atom is used in a tree without a Provider,
it will use the default state. This is so-called provider-less mode.
Providers are useful for three reasons:
- To provide a different state for each sub tree.
- To accept initial values of atoms.
- To clear all atoms by remounting.
const SubTree = () => (
<Provider>
<Child />
</Provider>
)Signatures
const Provider: React.FC<{
store?: Store
}>Atom configs don't hold values. Atom values reside in separate stores. A Provider is a component that contains a store and provides atom values under the component tree. A Provider works like React context provider. If you don't use a Provider, it works as provider-less mode with a default store. A Provider will be necessary if we need to hold different atom values for different component trees. Provider can take an optional prop store.
const Root = () => (
<Provider>
<App />
</Provider>
)`store` prop
A Provider accepts an optional prop store that you can use for the Provider subtree.
Example
const myStore = createStore()
const Root = () => (
<Provider store={myStore}>
<App />
</Provider>
)useStore
This hook returns a store within the component tree.
const Component = () => {
const store = useStore()
// ...
}Core Store (`docs/core/store.mdx`)
title: Store nav: 2.03 keywords: store,createstore,getdefaultstore,defaultstore
createStore
This function is to create a new empty store.
The store can be used to pass in Provider.
The store has three methods: get for getting atom values,set for setting atom values, and sub for subscribing to atom changes.
const myStore = createStore()
const countAtom = atom(0)
myStore.set(countAtom, 1)
const unsub = myStore.sub(countAtom, () => {
console.log('countAtom value is changed to', myStore.get(countAtom))
})
// unsub() to unsubscribe
const Root = () => (
<Provider store={myStore}>
<App />
</Provider>
)getDefaultStore
This function returns a default store that is used in provider-less mode.
const defaultStore = getDefaultStore()Core Use Atom (`docs/core/use-atom.mdx`)
title: useAtom nav: 2.02 keywords: use,useatom,useatomvalue,useatomvalueraw,useatomvaluerawsync,usesetatom,atomvalue,setatom
Choosing a hook
- Read and write an atom:
useAtom - Read an atom's value:
useAtomValue - Write an atom without reading it (avoids re-renders):
useSetAtom - Read without suspending on async atoms:
useAtomValueRaw - Same, with no risk of missing an update:
useAtomValueRawSync
useAtom
The useAtom hook is used to read an atom from the state.
The state can be seen as a WeakMap of atom configs and atom values.
The useAtom hook returns the atom value and an update function as a tuple,
just like React's useState.
It takes an atom config created with atom() as a parameter.
At the creation of the atom config, there is no value associated with it.
Only once the atom is used via useAtom, does the initial value get stored in the state.
If the atom is a derived atom, the read function is called to compute its initial value.
When an atom is no longer used, meaning all the components using it are unmounted
and the atom config no longer exists, the value in the state is garbage collected.
const [value, setValue] = useAtom(anAtom)The setValue takes just one argument, which will be passed
to the write function of the atom as the third parameter.
The end result depends on how the write function is implemented.
If the write function is not explicitly set, the atom will simply receive the value passed as a parameter to setValue.
Note: as mentioned in the atom section, referential equality is important when creating atoms,
so you need to handle it properly otherwise it can cause infinite loops.
const stableAtom = atom(0)
const Component = () => {
const [atomValue] = useAtom(atom(0)) // This will cause an infinite loop since the atom instance is being recreated in every render
const [atomValue] = useAtom(stableAtom) // This is fine
const [derivedAtomValue] = useAtom(
useMemo(
// This is also fine
() => atom((get) => get(stableAtom) * 2),
[],
),
)
}Note: Remember that React is responsible for calling your component, meaning it has to be idempotent, ready to be called multiple times. You will often see an extra re-render even if no props or atoms have changed. An extra re-render without a commit is an expected behavior, since it is the default behavior of useReducer in React 18.
Signatures
// primitive or writable derived atom
function useAtom<Value, Update>(
atom: WritableAtom<Value, Update>,
options?: { store?: Store },
): [Value, SetAtom<Update>]
// read-only atom
function useAtom<Value>(
atom: Atom<Value>,
options?: { store?: Store },
): [Value, never]How atom dependency works
Every time we invoke the "read" function, we refresh the dependencies and dependents.
The read function is the first parameter of the atom.
If B depends on A, it means that A is a dependency of B, and B is a dependent on A.
const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())When you create the atom, the dependency will not be present. On first use, we run the read function and conclude that uppercaseAtom depends on textAtom. So uppercaseAtom is added to the dependents of textAtom.
When we re-run the read function of uppercaseAtomΒ (because its textAtom dependency is updated),
the dependency is created again, which is the same in this case. We then remove stale dependents from textAtom and replace them with their latest versions.
Atoms can be created on demand
While the basic examples here show defining atoms globally outside components,
there's no restrictions about where or when we can create an atom.
As long as we remember that atoms are identified by their object referential identity,
we can create them anytime.
If you create atoms in render functions, you would typically want to use
a hook like useRef or useMemo for memoization. If not, the atom would be re-created each time the component renders.
You can create an atom and store it with useState or even in another atom.
See an example in issue #5.
You can also cache atoms somewhere globally.
See this example or
that example.
Check atomFamily in utils for parameterized atoms.
useAtomValue
const countAtom = atom(0)
const Counter = () => {
const setCount = useSetAtom(countAtom)
const count = useAtomValue(countAtom)
return (
<>
<div>count: {count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
)
}Similar to the useSetAtom hook, useAtomValue allows you to access a read-only atom. Nonetheless, it can also be used to access read-write atom's values.
Under the hood, useAtomValue is useAtomValueRaw plus React's use(): it resolves promises and suspends, instead of returning them as-is.
A subtle mount-timing change in v3
In v2, useAtomValue always re-rendered once right after mounting, even without a value change. This caught updates that happened between the initial render and the subscription, for example a value written by a child's useEffect (which runs before the parent's).
In v3, that extra render is gone: useAtomValue only re-renders on an actual change. It's faster, but it can miss that specific kind of update until the next one. Use useAtomValueRawSync if you need to catch it.
useAtomValueRaw
β οΈ This hook is only available since Jotai v3, and it's intended for advanced users.
const countAtom = atom(0)
const Counter = () => {
const count = useAtomValueRaw(countAtom)
// ...
}useAtomValueRaw is like useAtomValue, but it doesn't resolve promises: the value of an async atom is returned as a promise, and the component never suspends. It subscribes to the store in an effect, so it doesn't cause an extra re-render on initial mount. As a trade-off, a value change that happens between the initial render and the subscription is not picked up until the next change.
"Raw" means the promise is returned without being consumed through React's
use().
useAtomValueRawSync
β οΈ This hook is only available since Jotai v3, and it's intended for advanced users.
const countAtom = atom(0)
const Counter = () => {
const count = useAtomValueRawSync(countAtom)
// ...
}useAtomValueRawSync has the same signature as useAtomValueRaw and doesn't resolve promises either, but it's implemented with useSyncExternalStore. It avoids tearing and picks up values written during mount. As a trade-off, updates are always rendered synchronously without taking advantage of concurrent rendering features.
"Sync" refers to
useSyncExternalStore: rendering is always synchronous, with no concurrent-rendering support.
useSetAtom
const switchAtom = atom(false)
const SetTrueButton = () => {
const setCount = useSetAtom(switchAtom)
const setTrue = () => setCount(true)
return (
<div>
<button onClick={setTrue}>Set True</button>
</div>
)
}
const SetFalseButton = () => {
const setCount = useSetAtom(switchAtom)
const setFalse = () => setCount(false)
return (
<div>
<button onClick={setFalse}>Set False</button>
</div>
)
}
export default function App() {
const state = useAtomValue(switchAtom)
return (
<div>
State: <b>{state.toString()}</b>
<SetTrueButton />
<SetFalseButton />
</div>
)
}In case you need to update a value of an atom without reading it, you can use useSetAtom().
This is especially useful when the performance is a concern, as the const [, setValue] = useAtom(valueAtom) will cause unnecessary rerenders on each valueAtom update.
Extensions Cache (`docs/extensions/cache.mdx`)
title: Cache nav: 4.07 keywords: cache
Jotai provides primitive functions to optimize re-renders.
It's designed to hold only "current" atom values,
and it doesn't cache older values.
Caching is sometimes useful. For example, if an async atom
triggers network requests, we may want to cache the responses.
jotai-cache is
a third-party library to help such use cases.
Install
npm install jotai-cacheatomWithCache
atomWithCache(read, options): AtomatomWithCache creates a new read-only atom with cache.
Parameters
read: read function to define the read-only atom.
options (optional): an object of options to customize the behavior of the atom
Options
size (optional): maximum size of cache items.
shouldRemove (optional): a function to check if cache items should be removed.
areEqual (optional): a function to compare atom values.
Examples
/* Detailed source-code truncated for AI context efficiency. */Stackblitz
Extensions Effect (`docs/extensions/effect.mdx`)
title: Effect nav: 4.03 keywords: effect, atom effect, side effect, side-effect, sideeffect
jotai-effect is a utility package for reactive side effects in Jotai.
Install
npm install jotai-effectobserve
observe mounts an effect to watch state changes on a Jotai store. It's useful for running global side effects or logic at the store level.
If you don't have access to the store object and are not using the default store, use atomEffect or withAtomEffect instead.
Signature
type Cleanup = () => void
type Effect = (
get: Getter & { peek: Getter }
set: Setter & { recurse: Setter }
) => Cleanup | void
type Unobserve = () => void
function observe(effect: Effect, store?: Store): Unobserveeffect: A function for observing and reacting to atom state changes.
store: A Jotai store to mount the effect on. Defaults to the global store if not provided.
returns: A stable function that removes the effect from the store and cleans up any internal references.
Usage
import { observe } from 'jotai-effect'
const unobserve = observe((get, set) => {
set(logAtom, `someAtom changed: ${get(someAtom)}`)
})
unobserve()This allows you to run Jotai state-dependent logic outside React's lifecycle, ideal for application-wide effects.
Usage With React
Pass the store to both observe and the Provider to ensure the effect is mounted to the correct store.
const store = createStore()
const unobserve = observe((get, set) => {
set(logAtom, `someAtom changed: ${get(someAtom)}`)
}, store)
<Provider store={store}>...</Provider>atomEffect
atomEffect creates an atom for declaring side effects that react to state changes when mounted.
Signature
function atomEffect(effect: Effect): Atom<void>effect: A function for observing and reacting to atom state changes.
Usage
import { atomEffect } from 'jotai-effect'
const logEffect = atomEffect((get, set) => {
set(logAtom, get(someAtom)) // Runs on mount or when someAtom changes
return () => {
set(logAtom, 'unmounting') // Cleanup on unmount
}
})
// activates the atomEffect while Component is mounted
function Component() {
useAtom(logEffect)
}withAtomEffect
withAtomEffect binds an effect to a clone of the target atom. The effect is active while the cloned atom is mounted.
Signature
function withAtomEffect<T>(targetAtom: Atom<T>, effect: Effect): Atom<T>targetAtom: The atom to which the effect is bound.
effect: A function for observing and reacting to atom state changes.
Returns: An atom that is equivalent to the target atom but having a bound effect.
Usage
import { withAtomEffect } from 'jotai-effect'
const valuesAtom = withAtomEffect(atom(null), (get, set) => {
set(valuesAtom, get(countAtom))
return () => {
// cleanup
}
})Dependency Management
Aside from mount events, the effect runs when any of its dependencies change value.
Sync:
All atoms accessed withgetinside the effect are added to the atom's dependencies.Example
jsatomEffect((get, set) => { // updates whenever `anAtom` changes value get(anAtom) })Async:
Asynchronousgetcalls do not add dependencies.Example
jsatomEffect((get, set) => { setTimeout(() => { // does not add `anAtom` as a dependency get(anAtom) }) })Cleanup:
getcalls in cleanup do not add dependencies.Example
jsatomEffect((get, set) => { return () => { // does not add `anAtom` as a dependency get(anAtom) } })Dependency Map Recalculation:
Dependencies are recalculated on every run.Example
jsatomEffect((get, set) => { if (get(isEnabledAtom)) { // `isEnabledAtom` and `anAtom` are dependencies const aValue = get(anAtom) } else { // `isEnabledAtom` and `anotherAtom` are dependencies const anotherValue = get(anotherAtom) } })
Effect Behavior
Executes Synchronously:
effectruns synchronous in the current task after synchronous evaluations complete.Example
jsconst logCounts = atomEffect((get, set) => { set(logAtom, `count is ${get(countAtom)}`) }) const actionAtom = atom(null, (get, set) => { get(logAtom) // 'count is 0' set(countAtom, (value) => value + 1) get(logAtom) // 'count is 0' - the effect runs once the write function completes }) store.sub(logCounts, () => {}) store.set(actionAtom) store.get(logAtom) // 'count is 1' - the effect ran before `store.set` returnedBatched Updates:
Multiple synchronous updates are batched as a single atomic transaction.Example
jsconst tensAtom = atom(0) const onesAtom = atom(0) const updateTensAndOnes = atom(null, (get, set) => { set(tensAtom, (value) => value + 1) set(onesAtom, (value) => value + 1) }) const combos = atom([]) const effectAtom = atomEffect((get, set) => { const value = get(tensAtom) * 10 + get(onesAtom) set(combos, (arr) => [...arr, value]) }) store.sub(effectAtom, () => {}) store.set(updateTensAndOnes) store.get(combos) // [00, 11]Resistant to Infinite Loops:
atomEffectavoids rerunning when it updates a value that it is watching.Example
jsatomEffect((get, set) => { get(countAtom) set(countAtom, (value) => value + 1) // Will not loop })Cleanup Function:
The cleanup function is invoked on unmount or before re-evaluation.Example
jsatomEffect((get, set) => { const intervalId = setInterval(() => set(clockAtom, Date.now())) return () => clearInterval(intervalId) })Idempotency:
atomEffectruns once per state change, regardless of how many times it is referenced.Example
jslet i = 0 const effectAtom = atomEffect(() => { get(countAtom) i++ }) store.sub(effectAtom, () => {}) store.sub(effectAtom, () => {}) store.set(countAtom, (value) => value + 1) console.log(i) // 1Conditionally Running Effects:
atomEffectonly runs when mounted.Example
jsatom((get) => { if (get(isEnabledAtom)) { get(effectAtom) } })Supports Peek:
Useget.peekto read atom data without subscribing.Example
jsconst countAtom = atom(0) atomEffect((get, set) => { const count = get.peek(countAtom) // Will not add `countAtom` as a dependency })Supports Recursion:
Recursion is supported withset.recursebut not in cleanup.Example
jsatomEffect((get, set) => { const count = get(countAtom) if (count % 10 === 0) { return } set.recurse(countAtom, (value) => value + 1) })
Extensions Immer (`docs/extensions/immer.mdx`)
title: Immer nav: 4.04 keywords: immer
Install
You have to install immer and jotai-immer to use this feature.
npm install immer jotai-immeratomWithImmer
atomWithImmer creates a new atom similar to the regular atom with a different writeFunction. In this bundle, we don't have read-only atoms, because the point of these functions is the immer produce(mutability) function.
The signature of writeFunction is (get, set, update: (draft: Draft<Value>) => void) => void.
import { useAtom } from 'jotai'
import { atomWithImmer } from 'jotai-immer'
const countAtom = atomWithImmer({ value: 0 })
const Counter = () => {
const [count] = useAtom(countAtom)
return <div>count: {count.value}</div>
}
const Controls = () => {
const [, setCount] = useAtom(countAtom)
// setCount === update : (draft: Draft<Value>) => void
const inc = () =>
setCount((draft) => {
++draft.value
})
return <button onClick={inc}>+1</button>
}Examples
Check this example with atomWithImmer:
withImmer
withImmer takes an atom and returns a derived atom, same as atomWithImmer it has a different writeFunction.
import { useAtom, atom } from 'jotai'
import { withImmer } from 'jotai-immer'
const primitiveAtom = atom({ value: 0 })
const countAtom = withImmer(primitiveAtom)
const Counter = () => {
const [count] = useAtom(countAtom)
return <div>count: {count.value}</div>
}
const Controls = () => {
const [, setCount] = useAtom(countAtom)
// setCount === update : (draft: Draft<Value>) => void
const inc = () =>
setCount((draft) => {
++draft.value
})
return <button onClick={inc}>+1</button>
}Examples
Check this example with withImmer:
useImmerAtom
This hook takes an atom and replaces the atom's writeFunction with the new immer-like writeFunction like the previous helpers.
import { atom } from 'jotai'
import { useImmerAtom } from 'jotai-immer'
const primitiveAtom = atom({ value: 0 })
const Counter = () => {
const [count] = useImmerAtom(primitiveAtom)
return <div>count: {count.value}</div>
}
const Controls = () => {
const [, setCount] = useImmerAtom(primitiveAtom)
// setCount === update : (draft: Draft<Value>) => void
const inc = () =>
setCount((draft) => {
++draft.value
})
return <button onClick={inc}>+1</button>
}It would be better if you don't use withImmer and atomWithImmer with useImmerAtom because they provide the immer-like writeFunction and we don't need to create a new one.
You can use useSetImmerAtom if you need only the setter part of useImmerAtom.
Examples
Check this example with useImmerAtom:
Demo
Extensions Location (`docs/extensions/location.mdx`)
title: Location nav: 4.06 keywords: location,hash
To deal with window.location, we have some functions to create atoms.
Install
You have to install jotai-location to use this feature.
npm install jotai-locationatomWithLocation
atomWithLocation(options): PrimitiveAtomatomWithLocation creates a new atom that links to window.location.
Typically, you should only instantiate atomWithLocation once per application. This is because changes made to one instance might not be reflected in others.
As this atom is designed to synchronize with the window.location object, using multiple instances can lead to unpredictable behavior.
Parameters
options (optional): an object of options to customize the behavior of the atom
Options
preloaded (optional): preloaded location value to avoid getting location at initialization.
replace (optional): a boolean to indicate to use replaceState instead of pushState.
getLocation (optional): a custom function to get location value.
applyLocation (optional): a custom function to set location value.
subscribe (optional): a custom function to subscribe to location change.
Examples
import { useAtom } from 'jotai'
import { atomWithLocation } from 'jotai-location'
const locationAtom = atomWithLocation()
const App = () => {
const [loc, setLoc] = useAtom(locationAtom)
return (
<ul>
<li>
<button
style={{
fontWeight: loc.pathname === '/' ? 'bold' : 'normal',
}}
onClick={() => setLoc((prev) => ({ ...prev, pathname: '/' }))}
>
Home
</button>
</li>
<li>
<button
style={{
fontWeight:
loc.pathname === '/foo' && !loc.searchParams?.get('bar')
? 'bold'
: 'normal',
}}
onClick={() =>
setLoc((prev) => ({
...prev,
pathname: '/foo',
searchParams: new URLSearchParams(),
}))
}
>
Foo
</button>
</li>
<li>
<button
style={{
fontWeight:
loc.pathname === '/foo' && loc.searchParams?.get('bar') === '1'
? 'bold'
: 'normal',
}}
onClick={() =>
setLoc((prev) => ({
...prev,
pathname: '/foo',
searchParams: new URLSearchParams([['bar', '1']]),
}))
}
>
Foo?bar=1
</button>
</li>
</ul>
)
}Stackblitz
atomWithHash
atomWithHash(key, initialValue, options): PrimitiveAtomThis creates a new atom that is connected with URL hash.
The hash must be in the URLSearchParams format.
It's a two-way binding: changing the atom value will change the hash and
changing the hash will change the atom value.
This function works only with DOM.
Parameters
key (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage
initialValue (required): the initial value of the atom
options (optional): an object of options to customize the behavior of the atom
Options
serialize (optional): a custom function to serialize the atom value to the hash. Defaults to JSON.stringify.
deserialize (optional): a custom function to deserialize the hash to the atom value. Defaults to JSON.parse.
subscribe (optional): custom hash change subscribe function
setHash (optional): replaceState or a custom function that describes how hash gets updated on the side of the browser. Defaults to pushing a new entry to the history via window.location.hash = searchParams (jotai-location#4)
Examples
import { useAtom } from 'jotai'
import { atomWithHash } from 'jotai-location'
const countAtom = atomWithHash('count', 1)
const Counter = () => {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<div>count: {count}</div>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
)
}Stackblitz
Deleting Item
Please refer atomWithStorage
for the usage about deleting items.
Extensions Optics (`docs/extensions/optics.mdx`)
title: Optics nav: 4.09 keywords: optics
Install
You have to install optics-ts and jotai-optics to use this feature.
npm install optics-ts jotai-opticsfocusAtom
focusAtom creates a new atom, based on the focus that you pass to it. This creates a derived atom that will focus on the specified part of the atom,
and when the derived atom is updated, the derivee is notified of the update, and the equivalent update is done on the derivee.
See this:
const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}>
const derivedAtom = focusAtom(baseAtom, (optic) => optic.prop('a')) // PrimitiveAtom<number>So basically, we started with a PrimitiveAtom<{a: number}>, which has a getter and a setter, and then used focusAtom to zoom in on the a-property of
the baseAtom, and got a PrimitiveAtom<number>. What is noteworthy here is that this derivedAtom is not only a getter, it is also a setter. If derivedAtom is updated, then equivalent update is done on the baseAtom.
The example below is simple, but it's a starting point. focusAtom supports many kinds of optics, including Lens, Prism, Isomorphism.
To see more advanced optics, please see the example at: https://github.com/akheron/optics-ts
Example
import { atom } from 'jotai'
import { focusAtom } from 'jotai-optics'
const objectAtom = atom({ a: 5, b: 10 })
const aAtom = focusAtom(objectAtom, (optic) => optic.prop('a'))
const bAtom = focusAtom(objectAtom, (optic) => optic.prop('b'))
const Controls = () => {
const [a, setA] = useAtom(aAtom)
const [b, setB] = useAtom(bAtom)
return (
<div>
<span>Value of a: {a}</span>
<span>Value of b: {b}</span>
<button onClick={() => setA((oldA) => oldA + 1)}>Increment a</button>
<button onClick={() => setB((oldB) => oldB + 1)}>Increment b</button>
</div>
)
}Stackblitz
Extensions Query (`docs/extensions/query.mdx`)
title: Query nav: 4.02 keywords: tanstack,query
TanStack Query provides a set of functions for managing async state (typically external data).
From the Overview docs:
React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
jotai-tanstack-query is a Jotai extension library for TanStack Query. It provides a wonderful interface with all of the TanStack Query features, providing you the ability to use those features in combination with your existing Jotai state.
Support
jotai-tanstack-query currently supports TanStack Query v5.
Install
In addition to jotai, you have to install jotai-tanstack-query and @tanstack/query-core to use the extension.
npm install jotai-tanstack-query @tanstack/query-coreIncremental Adoption
You can incrementally adopt jotai-tanstack-query in your app. It's not an all or nothing solution. You just have to ensure you are using the same QueryClient instance. QueryClient Setup.
// existing useQueryHook
const { data, isPending, isError } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
// jotai-tanstack-query
const todosAtom = atomWithQuery(() => ({
queryKey: ['todos'],
}))
const [{ data, isPending, isError }] = useAtom(todosAtom)Exported functions
atomWithQueryfor useQueryatomWithInfiniteQueryfor useInfiniteQueryatomWithMutationfor useMutationatomWithSuspenseQueryfor useSuspenseQueryatomWithSuspenseInfiniteQueryfor useSuspenseInfiniteQueryatomWithMutationStatefor useMutationState
All functions follow the same signature.
const dataAtom = atomWithSomething(getOptions, getQueryClient)The first getOptions parameter is a function that returns an input to the observer.
The second optional getQueryClient parameter is a function that return QueryClient.
atomWithQuery usage
atomWithQuery creates a new atom that implements a standard Query from TanStack Query.
import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
}))
const UserData = () => {
const [{ data, isPending, isError }] = useAtom(userAtom)
if (isPending) return <div>Loading...</div>
if (isError) return <div>Error</div>
return <div>{JSON.stringify(data)}</div>
}atomWithInfiniteQuery usage
atomWithInfiniteQuery is very similar to atomWithQuery, however it is for an InfiniteQuery, which is used for data that is meant to be paginated. You can read more about Infinite Queries here.
Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.
A notable difference between a standard query atom is the additional option getNextPageParam and getPreviousPageParam, which is what you'll use to instruct the query on how to fetch any additional pages.
import { atom, useAtom } from 'jotai'
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'
const postsAtom = atomWithInfiniteQuery(() => ({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
initialPageParam: 1,
}))
const Posts = () => {
const [{ data, fetchNextPage, isPending, isError, isFetching }] =
useAtom(postsAtom)
if (isPending) return <div>Loading...</div>
if (isError) return <div>Error</div>
return (
<>
{data.pages.map((page, index) => (
<div key={index}>
{page.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button onClick={() => fetchNextPage()}>Next</button>
</>
)
}atomWithMutation usage
atomWithMutation creates a new atom that implements a standard Mutation from TanStack Query.
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
const postAtom = atomWithMutation(() => ({
mutationKey: ['posts'],
mutationFn: async ({ title }: { title: string }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
method: 'POST',
body: JSON.stringify({
title,
body: 'body',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const data = await res.json()
return data
},
}))
const Posts = () => {
const [{ mutate, status }] = useAtom(postAtom)
return (
<div>
<button onClick={() => mutate({ title: 'foo' })}>Click me</button>
<pre>{JSON.stringify(status, null, 2)}</pre>
</div>
)
}atomWithMutationState usage
atomWithMutationState creates a new atom that gives you access to all mutations in the MutationCache.
const mutationStateAtom = atomWithMutationState((get) => ({
filters: {
mutationKey: ['posts'],
},
}))Suspense
jotai-tanstack-query can also be used with React's Suspense.
atomWithSuspenseQuery usage
import { atom, useAtom } from 'jotai'
import { atomWithSuspenseQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const userAtom = atomWithSuspenseQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
}))
const UserData = () => {
const [{ data }] = useAtom(userAtom)
return <div>{JSON.stringify(data)}</div>
}atomWithSuspenseInfiniteQuery usage
import { atom, useAtom } from 'jotai'
import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query'
const postsAtom = atomWithSuspenseInfiniteQuery(() => ({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
initialPageParam: 1,
}))
const Posts = () => {
const [{ data, fetchNextPage, isPending, isError, isFetching }] =
useAtom(postsAtom)
return (
<>
{data.pages.map((page, index) => (
<div key={index}>
{page.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button onClick={() => fetchNextPage()}>Next</button>
</>
)
}Referencing the same instance of Query Client in your project
Perhaps you have some custom hooks in your project that utilises the useQueryClient() hook to obtain the QueryClient object and call its methods.
To ensure that you reference the same QueryClient object, be sure to wrap the root of your project in a `` and initialise queryClientAtom with the same queryClient value you provided to QueryClientProvider.
Without this step, useQueryAtom will reference a separate QueryClient from any hooks that utilise the useQueryClient() hook to get the queryClient.
Alternatively, you can specify your queryClient with getQueryClient parameter.
Example
In the example below, we have a mutation hook, useTodoMutation and a query todosAtom.
We included an initialisation step in our root <App> node.
Although they reference methods same query key ('todos'), the onSuccess invalidation in useTodoMutation will not trigger if the Provider initialisation step was not done.
This will result in todosAtom showing stale data as it was not prompted to refetch.
β οΈ Note: When using Typescript, it is recommended to use a Map when passing the queryClient value to useHydrateAtoms. You can find a working example in the Initializing State on Render docs
import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import {
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient()
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
{/*
This Provider initialisation step is needed so that we reference the same
queryClient in both atomWithQuery and other parts of the app. Without this,
our useQueryClient() hook will return a different QueryClient object
*/}
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
</QueryClientProvider>
)
}
export const todosAtom = atomWithQuery((get) => {
return {
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
}
})
export const useTodoMutation = () => {
const queryClient = useQueryClient()
return useMutation(
async (body: todo) => {
await fetch('/todo', { Method: 'POST', Body: body })
},
{
onSuccess: () => {
void queryClient.invalidateQueries(['todos'])
},
onError,
}
)
}SSR support
All atoms can be used within the context of a server side rendered app, such as a next.js app or Gatsby app. You can use both options that React Query supports for use within SSR apps, hydration or initialData.
Error handling
Fetch error will be thrown and can be caught with ErrorBoundary.
Refetching may recover from a temporary error.
See a working example to learn more.
Devtools
In order to use the Devtools, you need to install it additionally.
npm install @tanstack/react-query-devtoolsAll you have to do is put the <ReactQueryDevtools /> within <QueryClientProvider />.
import {
QueryClientProvider,
QueryClient,
QueryCache,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
<ReactQueryDevtools />
</QueryClientProvider>
)
}Migrate to v0.8.0
Change in atom signature
All atom signatures have changed to be more consistent with TanStack Query.
v0.8.0 returns only a single atom, instead of a tuple of atoms, and hence the name change from atomsWithSomething toatomWithSomething.
- const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient)
+ const dataAtom = atomWithSomething(getOptions, getQueryClient)Simplified Return Structure
In the previous version of jotai-tanstack-query, the query atoms atomsWithQuery and atomsWithInfiniteQuery returned a tuple of atoms: [dataAtom, statusAtom]. This design separated the data and its status into two different atoms.
atomWithQuery and atomWithInfiniteQuery
dataAtomwas used to access the actual data (TData).statusAtomprovided the status object (QueryObserverResult), which included additional attributes likeisPending,isError, etc.
In v0.8.0, they have been replaced by atomWithQuery and atomWithInfiniteQuery to return only a single dataAtom. This dataAtom now directly provides the QueryObserverResult, aligning it closely with the behavior of Tanstack Query's bindings.
To migrate to the new version, replace the separate dataAtom and statusAtom usage with the unified dataAtom that now contains both data and status information.
- const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
- const [data] = useAtom(dataAtom);
- const [status] = useAtom(statusAtom);
+ const dataAtom = atomWithQuery(/* ... */);
+ const [{ data, isPending, isError }] = useAtom(dataAtom);atomWithMutation
Similar to atomsWithQuery and atomsWithInfiniteQuery, atomWithMutation also returns a single atom instead of a tuple of atoms. The return type of the atom value is MutationObserverResult.
- const [, postAtom] = atomsWithMutation(/* ... */);
- const [post, mutate] = useAtom(postAtom); // Accessing mutation status from post; and mutate() to execute the mutation
+ const postAtom = atomWithMutation(/* ... */);
+ const [{ data, error, mutate }] = useAtom(postAtom); // Accessing mutation result and mutate method from the same atomExamples
Basic demo
Devtools demo
Hackernews
Extensions Redux (`docs/extensions/redux.mdx`)
title: Redux nav: 4.98 keywords: redux published: false
Jotai's state resides in React, but sometimes it would be nice
to interact with the world outside React.
Redux provides a store interface that can be used to store some values
and sync with atoms in Jotai.
Install
You have to install redux and jotai-redux to use this feature.
npm install redux jotai-reduxatomWithStore
atomWithStore creates a new atom with redux store.
It's two-way binding and you can change the value from both ends.
import { useAtom } from 'jotai'
import { atomWithStore } from 'jotai-redux'
import { createStore } from 'redux'
const initialState = { count: 0 }
const reducer = (state = initialState, action: { type: 'INC' }) => {
if (action.type === 'INC') {
return { ...state, count: state.count + 1 }
}
return state
}
const store = createStore(reducer)
const storeAtom = atomWithStore(store)
const Counter = () => {
const [state, dispatch] = useAtom(storeAtom)
return (
<>
count: {state.count}
<button onClick={() => dispatch({ type: 'INC' })}>button</button>
</>
)
}Examples
Extensions Relay (`docs/extensions/relay.mdx`)
title: Relay nav: 4.98 keywords: relay published: false
You can use Jotai with Relay.
Install
You have to install jotai-relay and relay-runtime.
npm install jotai-relay relay-runtimeUsage
See Relay Docs to
learn about basics and how to use compiler in advance.
atomWithQuery
atomWithQuery creates a new atom with fetchQuery.
import React, { Suspense } from 'react'
import { Provider, useAtom } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
import { environmentAtom, atomWithQuery } from 'jotai-relay'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
import graphql from 'babel-plugin-relay/macro'
const myEnvironment = new Environment({
network: Network.create(async (params, variables) => {
const response = await fetch('https://countries.trevorblades.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.text,
variables,
}),
})
return response.json()
}),
store: new Store(new RecordSource()),
})
const countriesAtom = atomWithQuery(
graphql`
query AppCountriesQuery {
countries {
name
}
}
`,
() => ({}),
)
const Main = () => {
const [data] = useAtom(countriesAtom)
return (
<ul>
{data.countries.map(({ name }) => (
<li key={name}>{name}</li>
))}
</ul>
)
}
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[environmentAtom, myEnvironment]])
return children
}
const App = () => {
return (
<Provider>
<HydrateAtoms>
<Suspense fallback="Loading...">
<Main />
</Suspense>
</HydrateAtoms>
</Provider>
)
}Examples
atomWithMutation
atomWithMutation creates a new atom with commitMutation.
FIXME: add code example and codesandbox
atomWithSubscription
atomWithSubscription creates a new atom with requestSubscription.
FIXME: add code example and codesandbox
Extensions Scope (`docs/extensions/scope.mdx`)
title: Scope nav: 4.08 keywords: scope
There are a few libraries to extend Jotai's usage in React.
`jotai-scope`
While Jotai's Provider allows to scope Jotai's store under a subtree,
we can't use the store above the tree within the subtree.
A workaround is to use store option in useAtom and other hooks.
Instead of specifying the store option,<ScopeProvider> lets you reuse the same atoms in different parts
of the React tree without sharing state while still being able to
read other atoms from the parent store.
Install
npm install jotai-scopeCounter Example
import { atom, useAtom } from 'jotai'
import { ScopeProvider } from 'jotai-scope'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
const [anotherCount, setAnotherCount] = useAtom(anotherCountAtom)
return (
<>
<div>
<span>count: {count}</span>
<button type="button" onClick={() => setCount((v) => v + 1)}>
increment
</button>
</div>
<div>
<span>another count: {anotherCount}</span>
<button type="button" onClick={() => setAnotherCount((v) => v + 1)}>
increment
</button>
</div>
</>
)
}
function App() {
return (
<div>
<Counter />
<ScopeProvider atoms={[countAtom]}>
<Counter />
</ScopeProvider>
</div>
)
}`createIsolation`
Both Jotai's Provider and jotai-scope's scoped provider
are still using global contexts.
If you are developing a library that depends on Jotai and
the library user may use Jotai separately in their apps,
they can share the same context. This can be troublesome
because they point to unexpected Jotai stores.
To avoid conflicting the contexts,
a utility function called createIsolation is exported from jotai-scope.
import { createIsolation } from 'jotai-scope'
const { Provider, useStore, useAtom, useAtomValue, useSetAtom } =
createIsolation()
function Library() {
return (
<Provider>
<LibraryComponent />
</Provider>
)
}`bunshi` (formerly `jotai-molecules`)
Jotai atoms provide a basic solution to optimize re-renders.
Atoms defined globally can depend on other atoms,
but they can't depend on props and state within a component tree.
It's possible to define atoms within a component tree,
but then you would need to pass those atoms in some ways
(for example, atoms-in-atom.)
bunshi is
a third-party library to help such use cases.
See Motivation for more details.
Install
npm install bunshiCounter Example
import { atom, useAtom } from 'jotai'
import { molecule, useMolecule, createScope, ScopeProvider } from 'bunshi/react'
const InitialCountScope = createScope({ initialCount: 0 })
const countMolecule = molecule((getMol, getScope) => {
const { initialCount } = getScope(InitialCountScope)
return atom(initialCount)
})
function Counter() {
const countAtom = useMolecule(countMolecule)
const [count, setCount] = useAtom(countAtom)
return (
<div>
{count} <button onClick={() => setCount((v) => v + 1)}>+1</button>
</div>
)
}
function App() {
return (
<div>
<h1>With initial value 1</h1>
<ScopeProvider scope={InitialCountScope} value={{ initialCount: 1 }}>
<Counter />
<Counter />
</ScopeProvider>
<h1>With initial value 2</h1>
<ScopeProvider scope={InitialCountScope} value={{ initialCount: 2 }}>
<Counter />
<Counter />
</ScopeProvider>
<h1>Default</h1>
<Counter />
<Counter />
</div>
)
}Extensions Trpc (`docs/extensions/trpc.mdx`)
title: tRPC nav: 4.01 keywords: rpc,trpc,typescript,t3
You can use Jotai with tRPC.
Install
You have to install jotai-trpc, @trpc/client and @trpc/server to use the extension.
npm install jotai-trpc @trpc/client @trpc/serverUsage
import { createTRPCJotai } from 'jotai-trpc'
const trpc = createTRPCJotai<MyRouter>({
links: [
httpLink({
url: myUrl,
}),
],
})
const idAtom = atom('foo')
const queryAtom = trpc.bar.baz.atomWithQuery((get) => get(idAtom))atomWithQuery
...atomWithQuery creates a new atom with "query". It internally uses Vanilla Client's ...query procedure.
import { atom, useAtom } from 'jotai'
import { httpLink } from '@trpc/client'
import { createTRPCJotai } from 'jotai-trpc'
import { trpcPokemonUrl } from 'trpc-pokemon'
import type { PokemonRouter } from 'trpc-pokemon'
const trpc = createTRPCJotai<PokemonRouter>({
links: [
httpLink({
url: trpcPokemonUrl,
}),
],
})
const NAMES = [
'bulbasaur',
'ivysaur',
'venusaur',
'charmander',
'charmeleon',
'charizard',
'squirtle',
'wartortle',
'blastoise',
]
const nameAtom = atom(NAMES[0])
const pokemonAtom = trpc.pokemon.byId.atomWithQuery((get) => get(nameAtom))
const Pokemon = () => {
const [data, refresh] = useAtom(pokemonAtom)
return (
<div>
<div>ID: {data.id}</div>
<div>Height: {data.height}</div>
<div>Weight: {data.weight}</div>
<button onClick={refresh}>Refresh</button>
</div>
)
}Examples
atomWithMutation
...atomWithMutation creates a new atom with "mutate". It internally uses Vanilla Client's ...mutate procedure.
FIXME: add code example and codesandbox
atomWithSubscription
...atomWithSubscription creates a new atom with "subscribe". It internally uses Vanilla Client's ...subscribe procedure.
FIXME: add code example and codesandbox
Extensions Urql (`docs/extensions/urql.mdx`)
title: URQL nav: 4.03 keywords: urql
urql offers a toolkit for GraphQL querying, caching, and state management.
From the Overview docs:
urql is a highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow. It's built to be both easy to use for newcomers to GraphQL, and extensible, to grow to support dynamic single-app applications and highly customized GraphQL infrastructure. In short, urql prioritizes usability and adaptability.
jotai-urql is a Jotai extension library for URQL. It offers a cohesive interface that incorporates all of URQL's GraphQL features, allowing you to leverage these functionalities alongside your existing Jotai state.
Install
You have to install jotai-urql, @urql/core and wonka to use the extension.
npm install jotai-urql @urql/core wonkaExported functions
atomWithQueryfor client.queryatomWithMutationfor client.mutationatomWithSubscriptionfor client.subscription
Basic usage
Query:
import { useAtom } from 'jotai'
const countQueryAtom = atomWithQuery<{ count: number }>({
query: 'query Count { count }',
getClient: () => client, // This option is optional if `useRehydrateAtom([[clientAtom, client]])` is used globally
})
const Counter = () => {
// Will suspend until first operation result is resolved. Either with error, partial data, data
const [operationResult, reexecute] = useAtom(countQueryAtom)
if (operationResult.error) {
// This shall be handled in the parent ErrorBoundary above
throw operationResult.error
}
// You have to use optional chaining here, as data may be undefined at this point (only in case of error)
return <>{operationResult.data?.count}</>
}Mutation:
import { useAtom } from 'jotai'
const incrementMutationAtom = atomWithMutation<{ increment: number }>({
query: 'mutation Increment { increment }',
})
const Counter = () => {
const [operationResult, executeMutation] = useAtom(incrementMutationAtom)
return (
<div>
<button
onClick={() =>
executeMutation().then((it) => console.log(it.data?.increment))
}
>
Increment
</button>
<div>{operationResult.data?.increment}</div>
</div>
)
}Simplified type of options passed to functions
type AtomWithQueryOptions<
Data = unknown,
Variables extends AnyVariables = AnyVariables,
> = {
// Supports string query, typed-document-node, document node etc.
query: DocumentInput<Data, Variables>
// Will be enforced dynamically based on generic/typed-document-node types.
getVariables?: (get: Getter) => Variables
getContext?: (get: Getter) => Partial<OperationContext>
getPause?: (get: Getter) => boolean
getClient?: (get: Getter) => Client
}
type AtomWithMutationOptions<
Data = unknown,
Variables extends AnyVariables = AnyVariables,
> = {
query: DocumentInput<Data, Variables>
getClient?: (get: Getter) => Client
}
// Subscription type is the same as AtomWithQueryOptionsDisable suspense
Usage of import { loadable } from "jotai/utils" is preferred instead as proven more stable. However is you still want that
here is how you do it:
import { suspenseAtom } from 'jotai-urql'
export const App = () => {
// We disable suspense for the entire app
useHydrateAtoms([[suspenseAtom, false]])
return <Counter />
}Useful helper hook
Here is the helper hook, to cover one rare corner case, and make use of these bindings similar to @tanstack/react-query
default behavior where errors are treated as errors (in case of Promise reject) and are handled mostly in the nearby
ErrorBoundaries. Only valid for suspended version.
useQueryAtomData
Neatly returns data after the resolution + handles all the error throwing/reexecute cases/corner cases.
Note that Type is overridden so data it never undefined nor null (unless that's expected return type of the query itself)
/* Detailed source-code truncated for AI context efficiency. */Basic demo
Referencing the same instance of the client for both atoms and urql provider
To ensure that you reference the same urqlClient object, be sure to wrap the root of your project in a `` and initialise clientAtom with the same urqlClient value you provided to UrqlProvider.
Without this step, you may end up specifying client each time when you use atomWithQuery. Now you can just ignore the optional getClient parameter, and it will use the client from the context.
import { Suspense } from 'react'
import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import { clientAtom } from 'jotai-urql'
import {
createClient,
cacheExchange,
fetchExchange,
Provider as UrqlProvider,
} from 'urql'
const urqlClient = createClient({
url: 'https://countries.trevorblades.com/',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => {
return { headers: {} }
},
})
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[clientAtom, urqlClient]])
return children
}
export default function MyApp({ Component, pageProps }) {
return (
<UrqlProvider value={urqlClient}>
<Provider>
<HydrateAtoms>
<Suspense fallback="Loading...">
<Component {...pageProps} />
</Suspense>
</HydrateAtoms>
</Provider>
</UrqlProvider>
)
}Extensions Valtio (`docs/extensions/valtio.mdx`)
title: Valtio nav: 4.98 keywords: valtio,proxy published: false
Jotai's state resides in React, but sometimes it would be nice
to interact with the world outside React.
Valtio provides a proxy interface that can be used to store some values
and sync with atoms in Jotai.
This only uses the vanilla api of valtio.
Install
You have to install valtio and jotai-valtio to use this feature.
npm install valtio jotai-valtioatomWithProxy
atomWithProxy creates a new atom with valtio proxy.
It's two-way binding and you can change the value from both ends.
import { useAtom } from 'jotai'
import { atomWithProxy } from 'jotai-valtio'
import { proxy } from 'valtio/vanilla'
const proxyState = proxy({ count: 0 })
const stateAtom = atomWithProxy(proxyState)
const Counter = () => {
const [state, setState] = useAtom(stateAtom)
return (
<>
count: {state.count}
<button
onClick={() => setState((prev) => ({ ...prev, count: prev.count + 1 }))}
>
inc atom
</button>
<button
onClick={() => {
++proxyState.count
}}
>
inc proxy
</button>
</>
)
}Parameters
atomWithProxy(proxyObject, options?)proxyObject (required): the Valtio proxy object you want to derive the atom from
options.sync (optional): makes the atom update synchronously instead of waiting for batched updates, similar to valtio/useSnapshot. This will result in more renders, but have more guarantees that it syncs with other Jotai atoms.
atomWithProxy(proxyObject, { sync: true })Examples
mutableAtom
mutableAtom wraps a value in a self-aware Valtio proxy. You can make changes to it in the same way you would to a normal js-object.
Count value is stored under the value property.
const countProxyAtom = mutableAtom(0)
function IncrementButton() {
const countProxy = useAtomValue(countProxyAtom)
return <button onClick={() => ++countProxy.value}>+</button>
}Parameters
mutableAtom(value, options?)value (required): the value to proxy.
options.proxyFn (optional): allows customization with proxyFn for custom proxy functions. Can be proxy (default) or a custom function.
Examples
Caution on Mutating Proxies
Be careful to not mutate the proxy directly in the atom's read function or during render. Doing so could cause an infinite render loop.
const countProxyAtom = mutableAtom(0)
atom(
(get) => {
const countProxy = get(countProxyAtom)
++countProxy.value // This will cause an infinite loop
},
(get, set) => {
const countProxy = get(countProxyAtom)
++countProxy.value // This is fine
},
)Extensions Xstate (`docs/extensions/xstate.mdx`)
title: XState nav: 4.05 keywords: xstate,machine,atomwithmachine
Jotai's state management is primitive and flexible,
but that sometimes means too free.
XState is a sophisticated library to provide
a better and safer abstraction for state management.
Install
You have to install xstate and jotai-xstate to use this feature.
npm install xstate jotai-xstateatomWithMachine
atomWithMachine creates a new atom with XState machine.
It receives a function getMachine to create a new machine.getMachine is invoked at the first use with get argument,
with which you can read other atom values.
/* Detailed source-code truncated for AI context efficiency. */Restartable machine stored in a global Provider (provider-less mode)
When your machine reaches its final state it cannot receive any more events.
If your atomWithMachine is initialized in global store (aka provider-less mode),
to restart it you need to send a RESTART event to your machine like so:
import { RESTART } from 'jotai-xstate'
const YourComponent = () => {
const [current, send] = useAtom(yourMachineAtom)
const isFinalState = current.matches('myFinalState')
useEffect(() => {
// restart globally initialized machine on component unmount
return () => {
if (isFinalState) send(RESTART)
}
}, [isFinalState])
}Examples
Check examples with atomWithMachine:
Restartable machine:
Tutorials
Check out a course about Jotai and XState.
Complex State Management in React with Jotai and XState
(Note: In the course, it uses jotai/xstate which is supersede by jotai-xstate.)
Extensions Zustand (`docs/extensions/zustand.mdx`)
title: Zustand nav: 4.98 keywords: zustand published: false
Jotai's state resides in React, but sometimes it would be nice
to interact with the world outside React.
Zustand provides a store interface that can be used to hold some values
and sync with atoms in Jotai.
This only uses the vanilla api of zustand.
Install
You have to install zustand and jotai-zustand to use this feature.
npm install zustand jotai-zustandatomWithStore
atomWithStore creates a new atom with zustand store.
It's two-way binding and you can change the value from both ends.
import { useAtom } from 'jotai'
import { atomWithStore } from 'jotai-zustand'
import create from 'zustand/vanilla'
const store = create(() => ({ count: 0 }))
const stateAtom = atomWithStore(store)
const Counter = () => {
const [state, setState] = useAtom(stateAtom)
return (
<>
count: {state.count}
<button
onClick={() => setState((prev) => ({ ...prev, count: prev.count + 1 }))}
>
button
</button>
</>
)
}Examples
Guides Async (`docs/guides/async.mdx`)
title: Async nav: 8.99 keywords: async published: false
Using async atoms, you gain access to real-world data while still managing them directly from your atoms and with incredible ease.
We can separate them in two main categories:
- Async read atoms: async request is started instantly as soon as you try to get its value. You could relate to them as "smart getters".
- Async write atoms: async request is started at a specific moment. You could relate to them as "actions".
Async read atom
The read function of an atom can return a promise.
const countAtom = atom(1)
const asyncAtom = atom(async (get) => get(countAtom) * 2)Jotai is inherently leveraging Suspense to handle asynchronous flows.
const ComponentUsingAsyncAtoms = () => {
const [num] = useAtom(asyncAtom)
// here `num` is always `number` even though asyncAtom returns a Promise
}
const App = () => {
return (
<Suspense fallback={/* What to show while suspended */}>
<ComponentUsingAsyncAtoms />
</Suspense>
)
}Alternatively, you could avoid the inherent suspending that Jotai does for you, by wrapping your atoms with the loadable API, or by reading the atom with useAtomValueRaw instead of useAtomValue.
If another atom uses an async atom, it will return a promise. So, we need to make the atom also async.
const anotherAtom = atom(async (get) => (await get(asyncAtom)) / 2)This also applies to an atom with write function.
const asyncAtom = atom(async (get) => ...)
const writeAtom = atom(null, async (get, set, payload) => {
await get(asyncAtom)
// ...
})Async write atom
Async write atoms are another kind of async atom. When the write function of atom returns a promise.
const countAtom = atom(1)
const asyncIncrementAtom = atom(null, async (get, set) => {
// await something
set(countAtom, get(countAtom) + 1)
})
const Component = () => {
const [, increment] = useAtom(asyncIncrementAtom)
const handleClick = () => {
increment()
}
// ...
}Async sometimes
An interesting pattern that can be achieved with Jotai is switching from async to sync to trigger suspending when wanted.
const request = async () =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) =>
res.json(),
)
const baseAtom = atom(0)
const Component = () => {
const [value, setValue] = useAtom(baseAtom)
const handleClick = () => {
setValue(request()) // Will suspend until request resolves
}
// ...
}Usage in TypeScript
In TypeScript atom(0) is inferred as PrimitiveAtom<number>. It cannot accept Promise<number> as a value so preceding code would not typecheck. To accommodate for that you need to type your atom explicitly and add Promise<number> as accepted value.
const baseAtom = atom<number | Promise<number>>(0) // Will accept sync and async valuesAsync forever
Sometimes you may want to suspend until an unpredetermined moment (or never).
const baseAtom = atom(new Promise(() => {})) // Will be suspend until set otherwiseSuspense
Async support is first class in Jotai. It fully leverages React Suspense at its core.
Technically, Suspense usage other than React.lazy is still unsupported / undocumented in React 17. If this is blocking, so you can still use the
loadableAPI to avoid suspending
To use async atoms, you need to wrap your component tree with <Suspense>.
If you have a
, place **at least one** `<Suspense>` inside said; otherwise, it may cause an endless loop while rendering the components.
const App = () => (
<Provider>
<Suspense fallback="Loading...">
<Layout />
</Suspense>
</Provider>
)Having more <Suspense>s in the component tree is also possible and must be considered to profit from Jotai inherent handling at best.
Guides Atoms In Atom (`docs/guides/atoms-in-atom.mdx`)
title: Atoms in atom nav: 8.12
atom() creates an atom config, which is an object, but it doesn't hold a value.
Atom configs don't have string keys and we identify them with referential equality.
In other words, we can use an atom config like a key.
Storing an atom config in useState
First things first, we can store an atom config in useState.
const Component = ({ atom1, atom2 }) => {
const [selectedAtom, setSelectedAtom] = useState(atom1)
const [value] = useAtom(selectedAtom)
return (
<div>
Selected value: {value}
<button onClick={() => setSelectedAtom(atom1)}>Select an atom</button>
<button onClick={() => setSelectedAtom(atom2)}>
Select another atom
</button>
</div>
)
}Note that we can pass atoms configs as props.
It might not make any sense, but we could create an atom config on demand.
const Component = () => {
const [currentAtom, setCurrentAtom] = useState(() => atom(0))
const [count, setCount] = useAtom(currentAtom)
return (
<div>
Count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCurrentAtom(atom(0))}>Create new</button>
</div>
)
}Storing an atom config in atom
Likewise, we can store an atom config as a value of another atom.
const firstNameAtom = atom('Tanjiro')
const lastNameAtom = atom('Kamado')
const showingNameAtom = atom(firstNameAtom)
const Component = () => {
const [nameAtom, setNameAtom] = useAtom(showingNameAtom)
const [name] = useAtom(nameAtom)
return (
<div>
Name: {name}
<button onClick={() => setNameAtom(firstNameAtom)}>
Show First Name
</button>
<button onClick={() => setNameAtom(lastNameAtom)}>Show Last Name</button>
</div>
)
}It's possible to create a derived atom.
const derivedNameAtom = atom((get) => {
const nameAtom = get(showingNameAtom)
return get(nameAtom)
})
// Or a shorter version
const derivedNameAtom = atom((get) => get(get(showingNameAtom)))To avoid confusing what is in atoms, naming atoms explicitly would be important.
Also, TypeScript type information would be helpful.
Storing an array of atom configs in atom
Finally, the atoms in atom pattern is to store an array of atom config into an atom.
const countsAtom = atom([atom(1), atom(2), atom(3)])
const Counter = ({ countAtom }) => {
const [count, setCount] = useAtom(countAtom)
return (
<div>
{count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
)
}
const Parent = () => {
const [counts, setCounts] = useAtom(countsAtom)
const addNewCount = () => {
const newAtom = atom(0)
setCounts((prev) => [...prev, newAtom])
}
return (
<div>
{counts.map((countAtom) => (
<Counter countAtom={countAtom} key={countAtom} />
))}
<button onClick={addNewCount}>Add</button>
</div>
)
}The benefit of this approach is, if you increment a count,
only the corresponding Counter component re-renders and no other components re-render.
It is important to note that anAtom.toString() returns a unique id, which can be used as a key in a map.
Hint for TypeScript users
<Counter countAtom={countAtom} key={`${countAtom}`} />Storing a map of atom configs in atom
Likewise, we can store an object map instead of an array.
const pricesAtom = atom({
apple: atom(15),
orange: atom(12),
pineapple: atom(25),
})
const Fruit = ({ name, priceAtom }) => {
const [price] = useAtom(priceAtom)
return (
<div>
{name}: {price}
</div>
)
}
const Parent = () => {
const [prices] = useAtom(pricesAtom)
return (
<div>
{Object.keys(prices).map((name) => (
<Fruit name={name} priceAtom={prices[name]} key={name} />
))}
</div>
)
}Guides Composing Atoms (`docs/guides/composing-atoms.mdx`)
title: Composing atoms nav: 8.11
The atom function provided by library is very primitive,
but it's also so flexible that you can combine multiple atoms
to implement a functionality.
Note again that
atom()creates an atom config, which is an object
to define a behavior of the atom.
Let's recap how we can derive an atom.
Basic derived atoms
Here's one of the simplest examples of a derived atom:
export const textAtom = atom('hello')
export const textLenAtom = atom((get) => get(textAtom).length)The textLenAtom is called read-only atom, because
it doesn't have a write function defined.
The following is another simple example with the write function:
const textAtom = atom('hello')
export const textUpperCaseAtom = atom(
(get) => get(textAtom).toUpperCase(),
(_get, set, newText) => set(textAtom, newText),
)In this case, textUpperCaseAtom is capable to set the original textAtom.
So, we can only export textUpperCaseAtom and can hidetextAtom in a smaller scope.
Now, let's see some real examples.
Overriding default atom values
Suppose we have a read-only atom.
Obviously read-only atoms are not writable, but we can combine two atoms
to override the read-only atom value.
const rawNumberAtom = atom(10.1) // can be exported
const roundNumberAtom = atom((get) => Math.round(get(rawNumberAtom)))
const overwrittenAtom = atom(null)
export const numberAtom = atom(
(get) => get(overwrittenAtom) ?? get(roundNumberAtom),
(get, set, newValue) => {
const nextValue =
typeof newValue === 'function' ? newValue(get(numberAtom)) : newValue
set(overwrittenAtom, nextValue)
},
)The final numberAtom just works like a normal primitive atom like atom(10).
If you set a number value, it will override the overwrittenAtom value,
and if you set null, it will be the roundNumberAtom value.
The reusable implementation is available as atomWithDefault
in jotai/utils. See atomWithDefault.
Next, let's see another example to sync with external value.
Syncing atom values with external values
There are some external values we want to deal with.localStorage is the one. Another is window.title.
Let's see how to create an atom that is in sync with localStorage.
const baseAtom = atom(localStorage.getItem('mykey') || '')
export const persistedAtom = atom(
(get) => get(baseAtom),
(get, set, newValue) => {
const nextValue =
typeof newValue === 'function' ? newValue(get(baseAtom)) : newValue
set(baseAtom, nextValue)
localStorage.setItem('mykey', nextValue)
},
)The persistedAtom works like a primitive atom, but its value
is persisted in localStorage.
The reusable implementation is available as atomWithStorage
in jotai/utils. See atomWithStorage.
There is a caveat with this usage. While atom config doesn't hold a value,
the external value is a singleton value.
So, if we use this atom in two different Providers,
There will be an inconsistency between the two persistedAtom values.
This could be solved if the external value had a subscription mechanism.
For example, atomWithProxy in jotai-valtio comes with subscription,
so we don't have such a limitation. Values in different Providers
will be in sync.
Back to the main topic, let's explore another example.
Extending atoms with `atomWith*` utils
We have several utils whose names start with atomWith.
They create an atom with a certain functionality.
Unfortunately, we can't combine two atom utils.
For example, atomWithStorage and atomWithReducer
can't be used to define a single atom.
In such a case, we need to derive an atom by ourselves.
Let's try adding reducer functionality to atomWithStorage:
const reducer = ...
const baseAtom = atomWithStorage('mykey', '')
export const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, action) => {
set(baseAtom, reducer(get(baseAtom), action))
}
)This is easy, because in this case, atomWithReducer
is a simple implementation compared to atomWithStorage.
For more complex cases, it wouldn't be very easy.
It would still be a open research field.
Finally, let's see another example with actions.
Action atoms
This should be known pattern as it's described in README.
Nonetheless, it might to be useful to revisit.
Let's create a counter that you can only increment or decrement by one.
One solution is atomWithReducer:
const countAtom = atomWithReducer(0, (prev, action) => {
if (action === 'INC') {
return prev + 1
}
if (action === 'DEC') {
return prev - 1
}
throw new Error('unknown action')
})This is fine, but not very atomic.
If we want to get benefit from code splitting / lazy loading,
We want to create write only atoms, or action atoms.
const baseAtom = atom(0) // do not export
export const countAtom = atom((get) => get(baseAtom)) // read only
export const incAtom = atom(null, (_get, set) => {
set(baseAtom, (prev) => prev + 1)
})
export const decAtom = atom(null, (_get, set) => {
set(baseAtom, (prev) => prev - 1)
})This is more atomic and looks like a Jotai way.
You can also create an action atom that will call another action atom:
// continued from the previous code
export const dispatchAtom = atom(null, (_get, set, action) => {
if (action === 'INC') {
set(incAtom)
} else if (action === 'DEC') {
set(decAtom)
} else {
throw new Error('unknown action')
}
})Why do we want it? Because it will be used only when needed.
It allows code splitting and dead code elimination.
In summary
Atoms are building block.
By composing atoms based on other atoms,
we can implement complicated logic.
This is not only for read derived atoms, but also for write action atoms.
Essentially, atoms are like functions, so composing atoms is
like composing functions with other functions.
When you compose atoms, prefer a wide graph over a deep one: an atom that derives from a few others is fine, but a long chain where each atom derives from the previous one is bounded by the JavaScript call stack and can overflow when read. See dependency graph depth in the performance guide for details and how to flatten such chains.
Note: We mentioned that our atoms can contain any kind of data, it can be a string, Blob, Observer, anything really. There is just one exception. Because derived atoms are defined using a function, Jotai will not understand if we pass it a function that isn't exactly a pure getter.
So what you can do is simply wrap your function in an object.
const doublerAtom = atom({ callback: (n) => n * 2 })
// Usage
const [doubler] = useAtom(doublerAtom)
const doubledValue = doubler.callback(50) // Will compute to 100Guides Core Internals (`docs/guides/core-internals.mdx`)
title: Core internals nav: 8.10
This guide is beneficial for those who want to understand the core implementation of Jotai. It's not meant to be a complete example of the
core implementation but rather a simplified version. It's inspired by the collection of tweets by Daishi Kato(@dai_shi).
First Version
Let's start with an easy example. An atom is just a function that will return a configuration object. We are using WeakMap to map atom with their state.
WeakMap doesn't keep its keys in memory, so if an atom is garbage collected, its state will be garbage collected too. This helps avoid memory leaks.
import { useState, useEffect } from 'react'
// atom function returns a config object which contains initial value
export const atom = (initialValue) => ({ init: initialValue })
// we need to keep track of the state of the atom.
// we are using weakmap to avoid memory leaks
const atomStateMap = new WeakMap()
const getAtomState = (atom) => {
let atomState = atomStateMap.get(atom)
if (!atomState) {
atomState = { value: atom.init, listeners: new Set() }
atomStateMap.set(atom, atomState)
}
return atomState
}
// useAtom hook returns a tuple of the current value
// and a function to update the atom's value
export const useAtom = (atom) => {
const atomState = getAtomState(atom)
const [value, setValue] = useState(atomState.value)
useEffect(() => {
const callback = () => setValue(atomState.value)
// same atom can be used at multiple components, so we need to
// keep listening for atom's state change till component is unmounted.
atomState.listeners.add(callback)
callback()
return () => atomState.listeners.delete(callback)
}, [atomState])
const setAtom = (nextValue) => {
atomState.value = nextValue
// let all the subscribed components know that the atom's state has changed
atomState.listeners.forEach((l) => l())
}
return [value, setAtom]
}Here's an example using our simplified atom implementation. Counter example
Ref tweet: Demystifying the internal of jotai
Second Version
Hang on! We can do better. In Jotai, we can create derived atom. A derived atom is an atom that depends on other atoms.
const priceAtom = atom(10)
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set, args) => {
set(priceAtom, get(priceAtom) - args)
},
)
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
// you can set as many atoms as you want at the same time
},
)To keep track of all the dependents, we need to add one more property to the atom's state. Let's say atom X depends on atom Y,
so when we update atom Y, we also update atom X. This is called dependency tracking.
const atomState = {
value: atom.init,
listeners: new Set(),
dependents: new Set(),
}We now need to create functions for reading and writing an atom that can handle updating dependent atoms' state.
/* Detailed source-code truncated for AI context efficiency. */Here is an example using our derived atom implementation. Derived counter example
Ref tweet: Supporting derived atoms
Guides Debugging (`docs/guides/debugging.mdx`)
title: Debugging nav: 8.07 keywords: debug,labels,devtools,freeze
In basic apps, console.log can be our best friend for debugging atoms, but when applications get bigger and we have more atoms to use, logging would not be a good way of debugging atoms.
Jotai provides two ways of debugging atoms, React Dev Tools and Redux Dev tools. For reading values and simple debugging, React Dev Tools might suit you, but for more complicated tasks like Time-travelling and setting values, Redux Dev Tools would be a better option.
Debug labels
It is worth mentioning that we have a concept called Debug labels in Jotai which may help us with debugging.
By default each Jotai state has the label like 1:<no debugLabel> with number being internal key assigned to each atom automatically. But you can add labels to atoms to help you distinguish them more easily with debugLabel.
const countAtom = atom(0)
// countAtom's debugLabel by default is 'atom1'
if (process.env.NODE_ENV !== 'production') {
countAtom.debugLabel = 'count'
// debugLabel is 'count' now
}Jotai provides both a Babel and a SWC plugin, that adds a debugLabel automatically to every atom, which makes things easier for us. For more info, check out jotai-babel and @swc-jotai/debug-label
Using React Dev Tools
You can use React Dev Tools
to inspect Jotai state. To achieve that useDebugValue
is used inside custom hooks. Keep in mind that it only works in dev mode
(such as NODE_ENV === 'development').
useAtom
useAtom calls useDebugValue for atom values, so if you select the component that consumes Jotai atoms in React Dev Tools, you would see "Atom" hooks for each atom that is used in the component along with the value it has right now.
useAtomsDebugValue
useAtomsDebugValue catches all atoms in a component tree under Provider (or an entire tree for Provider-less mode), and useDebugValue for all atoms values.
If you navigate to the component that has useAtomsDebugValue in the React Dev Tools, we can see a custom hook "AtomsDebugValue" which allows you to see all atom values and their dependents.
One use case is to put the hook just under the Provider component:
const DebugAtoms = () => {
useAtomsDebugValue()
return null
}
const Root = () => (
<Provider>
<DebugAtoms />
<App />
</Provider>
)Using Redux DevTools
You can also use Redux DevTools
to inspect atoms, with many features like Time-travelling and value dispatching.
[useAtomDevtools](https://jotai.org/docs/api/devtools#use-atom-devtools)
useAtomDevtoolsis a React hook that manages ReduxDevTools extension for a particular atom.
If you have a specific atom in mind that you may want to debug, useAtomDevtools can be a good option.
const countAtom = atom(0)
// setting countAtom.debugLabel is recommended if we have more atoms
function Counter() {
const [count, setCount] = useAtom(countAtom)
useAtomDevtools(countAtom)
}Now if we try setCount, we can see that the Redux Dev Tools logs those changes immediately.
Time travel
Sometimes we need to switch to a specific value of our atoms' state, with Time travelling this is possible.
You can hover on each action you see in the devtools and see the Jump option there, with clicking it you'd be able to switch to that specific value.
Pause
If we don't record changes on atoms, we can stop watching those using the Pausing feature.
Dispatch
It's possible to set values on atoms with the Dispatch feature. You can do that by clicking on the Show Dispatcher button.
This would set the countAtoms's value to 5.
We should note that the value will be parsed by JSON.parse, so pass supported values.
[useAtomsDevtools](../tools/devtools.mdx)
useAtomsDevtoolsis a catch-all version ofuseAtomDevtoolswhere it shows all atoms in the store instead of showing a specific one.
We'd recommend this hook if you want to keep track of all of your atoms in one place. It means every action on every atom that is placed in the bottom of this hook (in the React tree) will be caught by the Redux Dev Tools.
Every feature of useAtomDevtools is supported in this hook, but there's an extra feature, which includes giving more information about atoms dependents like:
{
"values": {
"atom1:count": 0,
"atom2:doubleCount": 0,
"atom3:half": 0,
"atom4:sum": 0
},
"dependents": {
"atom1:count": ["atom1:count", "atom2:doubleCount", "atom4:sum"],
"atom2:doubleCount": ["atom3:half", "atom4:sum"],
"atom3:half": ["atom4:sum"],
"atom4:sum": []
}
}Frozen Atoms
To find bugs where you accidentally tried to mutate objects stored in atoms you
could use freezeAtom or freezeAtomCreatorfrom jotai/utils bundle.
Which returns atoms value that is deeply freezed with Object.freeze.
freezeAtom
freezeAtom(anAtom): AtomTypefreezeAtom takes an existing atom and make it "frozen".
It returns the same atom.
The atom value will be deeply frozen by Object.freeze.
It is useful to find bugs where you unintentionally tried
to change objects (states) which can lead to unexpected behavior.
You may use freezeAtom with all atoms to prevent this situation.
Parameters
anAtom (required): An atom you wish to freeze.
Examples
import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'
const objAtom = freezeAtom(atom({ count: 0 }))freezeAtomCreator
If you need, you can define a factory for freezeAtom.
import { freezeAtom } from 'jotai/utils'
export function freezeAtomCreator<
CreateAtom extends (...args: unknown[]) => Atom<unknown>,
>(createAtom: CreateAtom): CreateAtom {
return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never
}Guides Initialize Atom On Render (`docs/guides/initialize-atom-on-render.mdx`)
title: Initializing state on render nav: 8.13
There are times when you need to create an reusable component which uses atoms.
These atoms' initial state are determined by the props passed to the component.
Below is a basic example illustrating how you can use Provider and its prop, initialValues, to initialize state.
Basic Example
CodeSandbox link: codesandbox.
Consider a basic example where you have a reusable TextDisplay component that allows you to display and update plain text.
This component has two child components, PrettyText and UpdateTextInput.
PrettyTextdisplays the text in blue.UpdateTextInputis an input field which updates the text value.
As opposed to passing text as a prop in the two child components, you decided that the text state should be shared between components as an atom.
To make TextDisplay component reusable, we take in a prop initialTextValue, which determines the initial state of the text atom.
To tie initialTextValue to textAtom, we wrap the child components in a component where we create a new store and pass it to a Provider component.
const textAtom = atom('')
const PrettyText = () => {
const [text] = useAtom(textAtom)
return (
<>
<text
style={{
color: 'blue',
}}
>
{text}
</text>
</>
)
}
const UpdateTextInput = () => {
const [text, setText] = useAtom(textAtom)
const handleInputChange = (e) => {
setText(e.target.value)
}
return (
<>
<input onChange={handleInputChange} value={text} />
</>
)
}
const HydrateAtoms = ({ initialValues, children }) => {
// initialising on state with prop on render here
useHydrateAtoms(initialValues)
return children
}
export const TextDisplay = ({ initialTextValue }) => (
<Provider>
<HydrateAtoms initialValues={[[textAtom, initialTextValue]]}>
<PrettyText />
<br />
<UpdateTextInput />
</HydrateAtoms>
</Provider>
)Now, we can easily reuse TextDisplay component with different initial text values despite them referencing the "same" atom.
export default function App() {
return (
<div className="App">
<TextDisplay initialTextValue="initial text value 1" />
<TextDisplay initialTextValue="initial text value 2" />
</div>
)
}This behavior is due to our child components looking for the lowest common Provider ancestor to derive its value.
For more information on Provider behavior, please read the docs here.
For more complex use cases, check out Scope extension.
Using Typescript
useHydrateAtoms has overloaded types and typescript cannot extract types from overloaded function. It is recommended to use a Map when passing initial atom values to the useHydrateAtoms.
Here is a working example:
import type { ReactNode } from 'react'
import { Provider, atom, useAtomValue } from 'jotai'
import type { WritableAtom } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
const testAtom = atom('')
export default function App() {
return (
<Provider>
<AtomsHydrator atomValues={[[testAtom, 'hello']]}>
<Component />
</AtomsHydrator>
</Provider>
)
}
//This component contains all the states and the logic
function Component() {
const testAtomValue = useAtomValue(testAtom)
return <div>{testAtomValue}</div>
}
function AtomsHydrator({
atomValues,
children,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
atomValues: Iterable<
readonly [WritableAtom<unknown, [any], unknown>, unknown]
>
children: ReactNode
}) {
useHydrateAtoms(new Map(atomValues))
return children
}Guides Migrating To V2 Api (`docs/guides/migrating-to-v2-api.mdx`)
title: v2 API migration nav: 8.0
RFC: https://github.com/pmndrs/jotai/discussions/1514
Jotai v1 is released at June 2022, and there has been various feedbacks.
React also proposes first-class support for promises.
Jotai v2 will have a new API.
Unfortunately, there are some breaking changes along with new features.
What are new features
Vanilla library
Jotai comes with vanilla (non-React) functions
and React functions separately.
They are provided from alternate entry points like jotai/vanilla.
Store API
Jotai exposes store interface so that you can directly manipulate atom values.
import { createStore } from 'jotai' // or from 'jotai/vanilla'
const store = createStore()
store.set(fooAtom, 'foo')
console.log(store.get(fooAtom)) // prints "foo"
const unsub = store.sub(fooAtom, () => {
console.log('fooAtom value in store is changed')
})
// call unsub() to unsubscribe.You can also create your own React Context to pass a store.
More flexible atom `write` function
The write function can accept multiple arguments,
and return a value.
atom(
(get) => get(...),
(get, set, arg1, arg2, ...) => {
...
return someValue
}
)What are breaking
Async atoms are no longer special
Async atoms are just normal atoms with promise values.
Atoms getter functions don't resolve promises.
On the other hand, useAtom hook continues to resolve promises.
Some utils like splitAtom expects sync atoms,
and won't work with async atoms.
Writable atom type is changed (TypeScript only)
// Old
WritableAtom<Value, Arg, Result extends void | Promise<void>>
// New
WritableAtom<Value, Args extends unknown[], Result>In general, we should avoid using WritableAtom type directly.
Some functions are dropped
- Provider's
initialValuesprop is removed, becausestoreis more flexible. - Provider's scope props is removed, because you can create own context.
abortableAtomutil is removed, because the feature is included by defaultwaitForAllutil is removed, becausePromise.alljust works
Migration guides
Async atoms
get function for read function of async atoms
doesn't resolve promises, so you have to put await or .then().
In short, the change is something like the following.
(If you are TypeScript users, types will tell where to changes.)
Previous API
const asyncAtom = atom(async () => 'hello')
const derivedAtom = atom((get) => get(asyncAtom).toUppercase())New API
const asyncAtom = atom(async () => 'hello')
const derivedAtom = atom(async (get) => (await get(asyncAtom)).toUppercase())
// or
const derivedAtom = atom((get) => get(asyncAtom).then((x) => x.toUppercase()))Provider's `initialValues` prop
Previous API
const countAtom = atom(0)
// in component
<Provider initialValues={[[countAtom, 1]]}>
...New API
const countAtom = atom(0)
const HydrateAtoms = ({ initialValues, children }) => {
useHydrateAtoms(initialValues)
return children
}
// in component
<Provider>
<HydrateAtoms initialValues={[[countAtom, 1]]}>
...Provider's `scope` prop
Previous API
const myScope = Symbol()
// Parent component
<Provider scope={myScope}>
...
</Provider>
// Child component
useAtom(..., myScope)New API
const MyContext = createContext()
const store = createStore()
// Parent component
<MyContext.Provider value={store}>
...
</MyContext.Provider>
// Child Component
const store = useContext(MyContext)
useAtom(..., { store })`abortableAtom` util
You no longer need the previous abortableAtom util,
because it's now supported with the normal atom.
Previous API
const asyncAtom = abortableAtom(async (get, { signal }) => {
...
}New API
const asyncAtom = atom(async (get, { signal }) => {
...
}`waitForAll` util
You no longer need the previous waitForAll util,
because we can use native Promise APIs.
Previous API
const allAtom = waitForAll([fooAtom, barAtom])New API
const allAtom = atom((get) => Promise.all([get(fooAtom), get(barAtom)]))Note that creating an atom in render function can cause infinite loop
`splitAtom` util (or some other utils) with async atoms
splitAtom util only accepts sync atoms.
You need to unwrap async atoms before passing.
This applies to some other utils like atomsWithQuery from jotai-tanstack-query.
Previous API
const splittedAtom = splitAtom(asyncArrayAtom)New API
const splittedAtom = splitAtom(unwrap(asyncArrayAtom, () => []))As of writing, unwrap is unstable and not documented.
You can instead use loadable, which gives more control on loading status.
If you need to use <Suspense>, atoms-in-atom pattern would help.
For more information, refer the following discussions:
- https://github.com/pmndrs/jotai/discussions/1615
- https://github.com/jotaijs/jotai-tanstack-query/issues/21
- https://github.com/pmndrs/jotai/discussions/1751
Some other changes
Utils
atomWithStorageutil'sdelayInitis removed as being default. Also it will always renderinitialValueon first render, and the stored value, if any, on subsequent renders. The new behavior differs from v1. See https://github.com/pmndrs/jotai/discussions/1737 for more information.useHydrateAtomscan only accept writable atoms.
Import statements
The v2 API is also provided from alternate entry points for library authors and non-React users.
jotai/vanillajotai/vanilla/utilsjotai/reactjotai/react/utils
// Available since v1.11.0
import { atom } from 'jotai/vanilla'
import { useAtom } from 'jotai/react'
// Available since v2.0.0
import { atom } from 'jotai' // is same as 'jotai/vanilla'
import { useAtom } from 'jotai' // is same as 'jotai/react'Note: If you are not using ESM, you want to prefer using jotai/vanilla etc. instead of jotai, for better tree shaking.
Guides Migrating To V3 (`docs/guides/migrating-to-v3.mdx`)
title: v3 migration nav: 8.01
Roadmap: https://github.com/pmndrs/jotai/discussions/2889
Jotai v3 focuses on modernizing the package.
The public API is unchanged: useAtom, useAtomValue and useSetAtom
keep the same signatures and the same behavior for the vast majority of use cases.
If your app runs on Jotai v2 without deprecation warnings,
it should run on v3 as is.
There is one subtle timing change worth knowing about, see
useAtomValue: a subtle mount-timing change
below.
Requirement changes
The minimum requirements are raised:
- React 18 (v2 supports React 17)
- TypeScript 5.5 (v2 supports TypeScript 3.8)
- Node.js
>=22.12.0(v2 supports Node.js 12.20)
Packaging changes
ESM only
The CJS, UMD and SystemJS builds are no longer provided.
Modern bundlers work with the ESM build out of the box.
The supported Node.js versions can load it
even from CJS code with require().
The package exports only expose the public entry points:
jotaijotai/utilsjotai/vanillajotai/vanilla/utilsjotai/vanilla/internalsjotai/reactjotai/react/utils
Importing other files in the package directly no longer works.
`process.env.NODE_ENV` is used directly
The published files read process.env.NODE_ENV directly
to omit development-only checks in production.
Bundlers handle it by default.
If you load Jotai in a browser without a bundler
(for example, with import maps),
you need to define it by yourself.
globalThis.process ??= { env: { NODE_ENV: 'production' } }ES2020 syntax
The published files are compiled with the ES2020 target.
If you need to support older browsers,
transpile the package in your build.
Removed features
`atomFamily` util
It's moved to the jotai-family package.
Previous API
import { atomFamily } from 'jotai/utils'New API
// npm install jotai-family
import { atomFamily } from 'jotai-family'`loadable` util
Use the unwrap util instead.
The previous behavior can be implemented in userland:
import { atom } from 'jotai'
import { unwrap } from 'jotai/utils'
function loadable(anAtom) {
const LOADING = { state: 'loading' }
const unwrappedAtom = unwrap(anAtom, () => LOADING)
return atom((get) => {
try {
const data = get(unwrappedAtom)
if (data === LOADING) {
return LOADING
}
return { state: 'hasData', data }
} catch (error) {
return { state: 'hasError', error }
}
})
}`jotai/babel` plugins
They are moved to the jotai-babel package.
Previous API
// babel config
plugins: ['jotai/babel/plugin-react-refresh']New API
// npm install jotai-babel
plugins: ['jotai-babel/plugin-react-refresh']`setSelf` option in the read function
Previous API
const anAtom = atom(async (get, { setSelf }) => {
// ...
})New API
There is no direct replacement.
Depending on the use case,onMount or jotai-effect
would cover it.
`delay` option in `useAtom` and `useAtomValue`
Previous API
const value = useAtomValue(anAtom, { delay: 100 })New API
Create a custom hook:
import { useEffect, useState } from 'react'
import { useStore } from 'jotai'
function useAtomValueWithDelay(anAtom, { delay }) {
const store = useStore()
const [value, setValue] = useState(() => store.get(anAtom))
useEffect(() => {
const unsub = store.sub(anAtom, () => {
setTimeout(() => setValue(store.get(anAtom)), delay)
})
return unsub
}, [store, anAtom, delay])
return value
}New hooks for advanced users
React 19 introduces React.use(), letting a component suspend by reading a
promise during render. useAtomValue uses it when available, and falls back
to the previous behavior on React 18. Splitting that logic out enables two
lower-level hooks:
useAtomValueRaw: likeuseAtomValue, but it never suspends. An async atom's value is returned
without being consumed through React'suse().useAtomValueRawSync: the same
idea, built onuseSyncExternalStore. It trades concurrent rendering for
no tearing and no missed update around mount.
You don't need either hook to migrate. useAtomValue keeps working as the
default choice. See Choosing a hook
for when to reach for them.
Guides Nextjs (`docs/guides/nextjs.mdx`)
title: Next.js nav: 8.03 keywords: next,nextjs
Hydration
Jotai has support for hydration of atoms with useHydrateAtoms. The documentation for the hook can be seen here.
Browser storage
Next.js still prerenders client components during static export, so browser storage APIs such as localStorage and sessionStorage are not available during the server render.
If you use atomWithStorage with createJSONStorage, avoid referencing browser storage globals directly. Use window behind a server guard instead:
import { atomWithStorage, createJSONStorage } from 'jotai/utils'
const storage = createJSONStorage(() =>
typeof window !== 'undefined' ? window.sessionStorage : undefined,
)
const someAtom = atomWithStorage('some-key', someInitialValue, storage)If the rendered UI depends on the stored value, render that subtree only on the client to avoid hydration mismatches:
import dynamic from 'next/dynamic'
const StoredPreferences = dynamic(
() => import('../components/StoredPreferences'),
{
ssr: false,
},
)See the storage documentation for more details.
Sync with router
It's possible to sync Jotai with the router. You can achieve this with atomWithHash:
const pageAtom = atomWithHash('page', 1, {
replaceState: true,
subscribe: (callback) => {
Router.events.on('routeChangeComplete', callback)
window.addEventListener('hashchange', callback)
return () => {
Router.events.off('routeChangeComplete', callback)
window.removeEventListener('hashchange', callback)
}
},
})This way you have full control over what router event you want to subscribe to.
In Next.js 13
As of Next.js 13 there have been some changes to the
Router.events.on()which no longer expose events. There are plans in the App Router Roadmap for event intercepting and hash handling. However there is no ETA on when this will be available or what it will look like. For now when trying to theatomWithHash()you will not get the atom loading with any data when navigating using the router, only when the page is reloaded or the component is rerendered. It is also recommended that you set thesetHashoption toreplaceStateas Next.js appears to use window.history in the background and this will allow the user to use the browser back button.
You can't return promises in server side rendering
It's important to note that you can't return promises with SSR - However, it's possible to guard against it inside the atom definition.
If possible use useHydrateAtoms to hydrate values from the server.
const postData = atom((get) => {
const id = get(postId)
if (isSSR || prefetchedPostData[id]) {
return prefetchedPostData[id] || EMPTY_POST_DATA
}
return fetchData(id) // returns a promise
})Provider
By default, Jotai uses an implicit global store to keep track of atom values. This is what is referred to as "provider-less" mode. This becomes an issue in SSR scenario because this global store is kept alive and is shared between multiple requests, which can lead to bugs and security risks.
To limit the lifetime of the store to the scope of one request, you need to use a Provider at the root of your app (or a subtree if you're using Jotai only for a part of your application).
import { Provider } from 'jotai'
function App({ Component, pageProps }: AppProps) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}In this case:
Providerwill hold the state of the atoms used in its subtree instead of the global store.Provider's lifetime will be the same as the app itself, and since the app is recreated on each SSR request we essentially limit the lifetime of the store to a single request as well.
SWC plugins
Jotai provides SWC plugins for better DX while developing with Next.js. Find more info in the SWC section.
Examples
Clock
HN Posts
Page Router demo:
App Router demo on Stackblitz
Next.js repo
npx create-next-app --example with-jotai with-jotai-appHere's a link.
Guides Performance (`docs/guides/performance.mdx`)
title: Performance nav: 8.08 keywords: performance
Note: This guide has room for improvement. Consider it as FYI for now.
Jotai & React gives us quite a few tools to manage the re-renders that happen in the app lifecycle.
First, please read about the difference between render & commit, because that's very important to understand before going further.
Cheap renders
As seen in the core section, due to React 18 default behaviour (but overall good practice), you have to make sure your component functions are idempotent.
They will be called multiple times during the render phase, even at mount. So we need to keep our renders cheap at all cost!
Heavy computation
Always make heavy computation outside of the React lifecycle (in actions for example)
Dont's:
// Heavy computation for each item
const selector = (s) => s.filter(heavyComputation)
const Profile = () => {
const [computed] = useAtom(selectAtom(friendsAtom, selector))
}Do's:
const friendsAtom = atom([])
const fetchFriendsAtom = atom(null, async (get, set, payload) => {
// Fetch all friends
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const data = await res.json()
// Make heavy computation once only
const computed = data.filter(heavyComputation)
set(friendsAtom, computed)
})
// Usage in components
const Profile = () => {
const [friends] = useAtom(friendsAtom)
}Small components
Observed atoms should only re-render small parts of your application that required an update. The less comparison React has to make, the shorter your render time will be.
Dont's:
const Profile = () => {
const [name] = useAtom(nameAtom)
const [age] = useAtom(ageAtom)
return (
<>
<div>{name}</div>
<div>{age}</div>
</>
)
}Do's:
const NameComponent = () => {
const [name] = useAtom(nameAtom)
return <div>{name}</div>
}
const AgeComponent = () => {
const [age] = useAtom(ageAtom)
return <div>{age}</div>
}
const Profile = () => {
return (
<>
<NameComponent />
<AgeComponent />
</>
)
}Render on demand
Usually, the main performance overhead will come from re-rendering parts of your app that did not need to, or way more than they should.
We have a few tools to deal with "when" React should render our components. If you have not seen the usage of useMemo and useCallback, please check the official React documentation for more info before going further.
They are of great use to reduce un-necessary renders where your app is not fluid.
But Jotai also provides its set of tools to handle the "when" our atoms should trigger a re-render.
- Out of the box, Jotai encourages you to split your data into atomic parts, hence each atom is stored separately and will only trigger a re-render when their own value change
selectAtomallows you to subscribe to specific part of a large object and only re-render on value changefocusAtomsame as selectAtom, but creating a new atom for the part, giving a setter to update that specific part easilysplitAtomdoes the work of selectAtom/focusAtom for a dynamic list
While this seems simplistic, it is simple to reason about. That was the goal, let's keep it simple to keep it fast.
Frequent or rare updates
Ask yourself whether your atom is usually going to be frequently update or more rarely.
Let's imagine an atom containing an object that changes almost every second, it may not be best suited to "focus" on a specific properties of this object using focusAtom, because anyway they will all re-render in the same time, so best adding no overhead and not create any more atoms.
On the other hand, if your object has properties that rarely change, and most importantly, that change independently from the other properties, then you may want to use focusAtom or selectAtom to prevent un-necessary renders.
Dependency graph depth
Jotai computes a derived atom by running its read function and synchronously reading each dependency it gets, which in turn reads their dependencies. This recursion runs on the JavaScript call stack, so the depth of a dependency chain is bounded by the engine's stack size, typically a few thousand levels depending on the engine and how much work each read does.
This is almost never a problem, because real graphs are wide rather than deep: an atom usually depends on a handful of others, not on a chain thousands of atoms long. Breadth is essentially unlimited; only chain length matters.
You can hit the limit by accident if you model a collection as a chain of derived atoms (each atom deriving from the previous one) instead of as data inside a single atom. Reading the end of a long enough chain throws a RangeError: Maximum call stack size exceeded (or too much recursion on Firefox). As of v2, this surfaces at the call site that triggered the read, rather than being silently swallowed, so you find out immediately instead of getting a corrupted graph.
Dont's:
// A linear chain: each atom depends on the previous one. Reading `last`
// recurses once per link, so a few thousand items can overflow the stack.
let prev = atom(0)
for (const value of veryLongList) {
const node = prev
prev = atom((get) => get(node) + value)
}
const last = prevDo's:
// Keep the collection in one atom and derive over it in a single read.
// The depth is constant (1) no matter how long the list is.
const listAtom = atom(veryLongList)
const totalAtom = atom((get) => get(listAtom).reduce((a, b) => a + b, 0))For a dynamic list whose items each need to be their own atom (for example, to render and update rows independently), reach for splitAtom rather than chaining. It gives you one atom per item that all derive directly from the source atom, so the graph stays one level deep instead of growing with the list.
If you genuinely need a deep computation, run it as a plain reduction inside a single atom's read (or in a write action) rather than spreading it across many chained atoms.
Guides Persistence (`docs/guides/persistence.mdx`)
title: Persistence nav: 8.14
Jotai has an atomWithStorage function in the utils bundle for persistence that supports persisting state in sessionStorage, localStorage, AsyncStorage, or the URL hash.
(Note: This guide is a bit outdated and requires some rewrites.)
There are also several alternate implementations here:
A simple pattern with localStorage
const strAtom = atom(localStorage.getItem('myKey') ?? 'foo')
const strAtomWithPersistence = atom(
(get) => get(strAtom),
(get, set, newStr) => {
set(strAtom, newStr)
localStorage.setItem('myKey', newStr)
},
)A helper function with localStorage and JSON parse
const atomWithLocalStorage = (key, initialValue) => {
const getInitialValue = () => {
const item = localStorage.getItem(key)
if (item !== null) {
return JSON.parse(item)
}
return initialValue
}
const baseAtom = atom(getInitialValue())
const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
const nextValue =
typeof update === 'function' ? update(get(baseAtom)) : update
set(baseAtom, nextValue)
localStorage.setItem(key, JSON.stringify(nextValue))
},
)
return derivedAtom
}(Error handling should be added.)
A helper function with AsyncStorage and JSON parse
This requires onMount.
const atomWithAsyncStorage = (key, initialValue) => {
const baseAtom = atom(initialValue)
baseAtom.onMount = (setValue) => {
;(async () => {
const item = await AsyncStorage.getItem(key)
setValue(JSON.parse(item))
})()
}
const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
const nextValue =
typeof update === 'function' ? update(get(baseAtom)) : update
set(baseAtom, nextValue)
AsyncStorage.setItem(key, JSON.stringify(nextValue))
},
)
return derivedAtom
}Don't forget to check out the Async documentation for more details on how to use async atoms.
Example with sessionStorage
Same as AsyncStorage, just use atomWithStorage util and override the default storage with the sessionStorage
import { atomWithStorage, createJSONStorage } from 'jotai/utils'
const storage = createJSONStorage(() =>
typeof window !== 'undefined' ? window.sessionStorage : undefined,
)
const someAtom = atomWithStorage('some-key', someInitialValue, storage)A serialize atom pattern
type Actions =
| { type: 'serialize'; callback: (value: string) => void }
| { type: 'deserialize'; value: string }
const serializeAtom = atom(null, (get, set, action: Actions) => {
if (action.type === 'serialize') {
const obj = {
todos: get(todosAtom).map(get),
}
action.callback(JSON.stringify(obj))
} else if (action.type === 'deserialize') {
const obj = JSON.parse(action.value)
// needs error handling and type checking
set(
todosAtom,
obj.todos.map((todo: Todo) => atom(todo)),
)
}
})
const Persist = () => {
const [, dispatch] = useAtom(serializeAtom)
const save = () => {
dispatch({
type: 'serialize',
callback: (value) => {
localStorage.setItem('serializedTodos', value)
},
})
}
const load = () => {
const value = localStorage.getItem('serializedTodos')
if (value) {
dispatch({ type: 'deserialize', value })
}
}
return (
<div>
<button onClick={save}>Save to localStorage</button>
<button onClick={load}>Load from localStorage</button>
</div>
)
}Examples
A pattern with atomFamily
type Actions =
| { type: 'serialize'; callback: (value: string) => void }
| { type: 'deserialize'; value: string }
const serializeAtom = atom(null, (get, set, action: Actions) => {
if (action.type === 'serialize') {
const todos = get(todosAtom)
const todoMap: Record<string, { title: string; completed: boolean }> = {}
todos.forEach((id) => {
todoMap[id] = get(todoAtomFamily({ id }))
})
const obj = {
todos,
todoMap,
filter: get(filterAtom),
}
action.callback(JSON.stringify(obj))
} else if (action.type === 'deserialize') {
const obj = JSON.parse(action.value)
// needs error handling and type checking
set(filterAtom, obj.filter)
obj.todos.forEach((id: string) => {
const todo = obj.todoMap[id]
set(todoAtomFamily({ id, ...todo }), todo)
})
set(todosAtom, obj.todos)
}
})
const Persist = () => {
const [, dispatch] = useAtom(serializeAtom)
const save = () => {
dispatch({
type: 'serialize',
callback: (value) => {
localStorage.setItem('serializedTodos', value)
},
})
}
const load = () => {
const value = localStorage.getItem('serializedTodos')
if (value) {
dispatch({ type: 'deserialize', value })
}
}
return (
<div>
<button onClick={save}>Save to localStorage</button>
<button onClick={load}>Load from localStorage</button>
</div>
)
}Examples
Guides React Native (`docs/guides/react-native.mdx`)
title: React Native nav: 8.06 keywords: native,ios,android
Jotai atoms can be used in React Native applications with absolutely no changes.
Our goal is to always be 100% compatible with React-Native.
Persistence
When it comes to persistence feature, the implementation specific to React Native are detailed in the atomWithStorage function in the utils bundle.
Performance
There is no known specific overhead when using Jotai in your app. Some libraries will add some/lots of additional properties and methods to the stored data for the practical usage, but Jotai behaves differently and you're always manipulating simple stuff that could barely be shortcuted.
Jotai atomic architecture will encourage you to split logic and data, providing a top-most experience to control every one of your render (or commits, to be precise) and therefore reach the best performances.
And always remember that renders have to be fast, split calculation logic to async actions.
Guides Remix (`docs/guides/remix.mdx`)
title: Remix nav: 8.05 keywords: remix status: draft
Hydration
Jotai has support for hydration of atoms with useHydrateAtoms. The documentation for the hook can be seen here.
Guides Resettable (`docs/guides/resettable.mdx`)
title: Resettable nav: 8.99 published: false
The Jotai core doesn't support resettable atoms.
But you can create those with helper functions from jotai/utils.
Primitive resettable atom with atomWithReset / useResetAtom
import { useAtom } from 'jotai'
import { atomWithReset, useResetAtom } from 'jotai/utils'
const todoListAtom = atomWithReset([
{ description: 'Add a todo', checked: false },
])
const TodoList = () => {
const [todoList, setTodoList] = useAtom(todoListAtom)
const resetTodoList = useResetAtom(todoListAtom)
return (
<>
<ul>
{todoList.map((todo) => (
<li>{todo.description}</li>
))}
</ul>
<button
onClick={() =>
setTodoList((l) => [
...l,
{
description: `New todo ${new Date().toDateString()}`,
checked: false,
},
])
}
>
Add todo
</button>
<button onClick={resetTodoList}>Reset</button>
</>
)
}Examples
Derived atom with RESET symbol
import { atom, useAtom, useSetAtom } from 'jotai'
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'
const dollarsAtom = atomWithReset(0)
const centsAtom = atom(
(get) => get(dollarsAtom) * 100,
(get, set, newValue: number | typeof RESET) =>
set(dollarsAtom, newValue === RESET ? newValue : newValue / 100)
)
const ResetExample = () => {
const [dollars] = useAtom(dollarsAtom)
const setCents = useSetAtom(centsAtom)
const resetCents = useResetAtom(centsAtom)
return (
<>
<h3>Current balance ${dollars}</h3>
<button onClick={() => setCents(100)}>Set $1</button>
<button onClick={() => setCents(200)}>Set $2</button>
<button onClick={resetCents}>Reset</button>
</>
)
}Examples
Guides Testing (`docs/guides/testing.mdx`)
title: Testing nav: 8.09 keywords: test,testing
We echo the guiding principles of Testing library:
- "The more your tests resemble the way your software is used, the more confidence they can give you."
We encourage you to write tests, like the user would interact with your atoms and components,
therefore treating Jotai as an implementation detail.
Here's an example using React testing library:
Counter.tsx:
import { atom, useAtom } from 'jotai'
export const countAtom = atom(0)
export function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<h1>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>one up</button>
</h1>
)
}Counter.test.ts:
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
test('should increment counter', () => {
// Arrange
render(<Counter />)
const counter = screen.getByText('0')
const incrementButton = screen.getByText('one up')
// Act
await userEvent.click(incrementButton)
// Assert
expect(counter.textContent).toEqual('1')
})Injected Values
You may want to inject arbitrary values to your atom before starting some tests.
Maybe the counter should be limited to 100. Let's see how to test that it doesn't increase after reaching 100.
In order to do that, simply use a Provider, and export your atom to be filled-in.
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useHydrateAtoms } from 'jotai/utils'
import { countAtom, Counter } from './Counter'
import { Provider } from 'jotai'
const HydrateAtoms = ({ initialValues, children }) => {
useHydrateAtoms(initialValues)
return children
}
const TestProvider = ({ initialValues, children }) => (
<Provider>
<HydrateAtoms initialValues={initialValues}>{children}</HydrateAtoms>
</Provider>
)
const CounterProvider = () => {
return (
<TestProvider initialValues={[[countAtom, 100]]}>
<Counter />
</TestProvider>
)
}
test('should not increment on max (100)', () => {
render(<CounterProvider />)
const counter = screen.getByText('100')
const incrementButton = screen.getByText('one up')
await userEvent.click(incrementButton)
expect(counter.textContent).toEqual('100')
})Custom hooks
If you have complex atoms, sometimes you want to test them in isolation.
For that, you can use React Hooks Testing Library.
Here's an example below:
countAtom.ts:
import { useAtom } from 'jotai'
import { atomWithReducer } from 'jotai/utils'
const reducer = (state: number, action?: 'INCREASE' | 'DECREASE') => {
switch (action) {
case 'INCREASE':
return state + 1
case 'DECREASE':
return state - 1
case undefined:
return state
}
}
export const countAtom = atomWithReducer(0, reducer)countAtom.test.ts:
import { renderHook, act } from '@testing-library/react-hooks'
import { useAtom } from 'jotai'
import { countAtom } from './countAtom'
test('should increment counter', () => {
const { result } = renderHook(() => useAtom(countAtom))
act(() => {
result.current[1]('INCREASE')
})
expect(result.current[0]).toBe(1)
})Example with React-Native
Of course, you can test React-Native components too the same way, with or without Provider.
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import { Counter } from './counter'
test('should increment counter', () => {
// Arrange
const { getByText } = render(<Counter />)
const counter = getByText('0')
const incrementButton = getByText('one up')
// Act
fireEvent.press(incrementButton)
// Assert
expect(counter.props.children.toString()).toEqual('1')
})Guides Typescript (`docs/guides/typescript.mdx`)
title: TypeScript nav: 8.02 keywords: typescript,types
Version requirement
Jotai requires TypeScript 5.5 or newer. Upgrade your TypeScript version if you're on 5.4 or older.
Jotai relies heavily on type inferences and requires strictNullChecks to be enabled. Consider adding "strict": true in your tsconfig.json.
#550
#802
#838
Notes
Primitive atoms are basically type inferred
const numAtom = atom(0) // primitive number atom
const strAtom = atom('') // primitive string atomPrimitive atoms can be explicitly typed
const numAtom = atom<number>(0)
const numAtom = atom<number | null>(0)
const arrAtom = atom<string[]>([])Derived atoms can mostly have their types inferred
In general, this is the recommended approach since typing derived
atoms can get a little confusing, particularly for those who are
doing it for the first time.
# Read only derived atoms
const readOnlyAtom = atom((get) => get(numAtom))
const asyncReadOnlyAtom = atom(async (get) => await get(someAsyncAtom))
# Write only atoms
const writeOnlyAtom = atom(null, (_get, set, str: string) => set(fooAtom, str))
const multipleArgumentsAtom = atom(
null,
(_get, set, valueOne: number, valueTwo: number) =>
set(fooAtom, Math.max(valueOne, valueTwo))
);
# Read/Write atoms
const readWriteAtom = atom(
(get) => get(strAtom),
(_get, set, num: number) => set(strAtom, String(num))
)
const asyncReadWriteAtom = atom(
async (get) => await get(asyncStrAtom),
(_get, set, num: number) => set(strAtom, String(num))
)Derived atoms can also be explicitly typed
If you encounter a situation where you need or want to explicitly
type your derived atoms, you can do that as well.
const asyncStrAtom = atom<Promise<string>>(async () => 'foo')
/**
* For write only atoms you'll need to supply three type parameters.
* The first type parameter describes the value returned from the atom. In the following example this is `null`.
* The second type parameter describes the arguments (plural) you will pass to the "write" function. Even if you only
* plan to have one argument, this type must be an array as show in the example.
* The third type parameter describes the return value of the "write" function. Normally, there is no return value,
* which is why we use `void` in the example below.
*/
const writeOnlyAtom = atom<null, [string, number], void>(
null,
(_get, set, stringValue, numberValue) => set(fooAtom, stringValue),
)
/**
* Read/Write atoms also take the same three type parameters.
* Just for the sake of completeness, in this example, we show that the first type parameter
* can also describe an async atom.
*/
const readWriteAtom = atom<Promise<string>, [number], void>(
async (get) => await get(asyncStrAtom),
(_get, set, num) => set(strAtom, String(num)),
)useAtom is typed based on atom types
const [num, setNum] = useAtom(primitiveNumAtom)
const [num] = useAtom(readOnlyNumAtom)
const [, setNum] = useAtom(writeOnlyNumAtom)Access to the value type of an atom
import { ExtractAtomValue, useAtomValue } from 'jotai'
import { userAtom } from 'state'
import { useQuery } from '@tanstack/react-query'
export default function WriteReview(hid) {
const user = useAtomValue(userAtom)
const res = useGetReviewQuery(user)
}
function useGetReviewQuery(user: ExtractAtomValue<typeof userAtom>) {
return fetch('/api/user/' + user.id + '/review')
}Guides Using Store Outside React (`docs/guides/using-store-outside-react.mdx`)
title: Using store outside React nav: 8.98 keywords: state, outside, react published: false
Jotai's state resides in React, but sometimes it would be nice
to interact with the world outside React.
createStore
createStore provides a store interface that can be used to store your atoms. Using the store, you can access and mutate the state of your stored atoms from outside React.
import { atom, useAtomValue, createStore, Provider } from 'jotai'
const timeAtom = atom(0)
const store = createStore()
store.set(timeAtom, (prev) => prev + 1) // Update atom's value
store.get(timeAtom) // Read atom's value
function Component() {
const time = useAtomValue(timeAtom) // Inside React
return (
<div className="App">
<h1>{time}</h1>
</div>
)
}
export default function App() {
return (
<Provider store={store}>
<Component />
</Provider>
)
}Examples
Guides Vite (`docs/guides/vite.mdx`)
title: Vite nav: 8.99 keywords: vite published: false
You can use the plugins from the jotai-babel package to enhance your developer experience when using Vite and Jotai.
In your vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
import jotaiDebugLabel from 'jotai-babel/plugin-debug-label'
import jotaiReactRefresh from 'jotai-babel/plugin-react-refresh'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
babel({
plugins: [jotaiDebugLabel, jotaiReactRefresh],
}),
],
// ... The rest of your configuration
})There's a template below to try it yourself.
Examples
Vite Template
Guides Waku (`docs/guides/waku.mdx`)
title: Waku nav: 8.04 keywords: waku status: draft
Hydration
Jotai has support for hydration of atoms with useHydrateAtoms. The documentation for the hook can be seen here.
Recipes Atom With Broadcast (`docs/recipes/atom-with-broadcast.mdx`)
title: atomWithBroadcast nav: 9.09 keywords: creators,broadcast
atomWithBroadcastcreates an atom. The atom will be shared between
browser tabs and frames, similar toatomWithStoragebut with the
initialization limitation.
This can be useful when you want states to interact with each other without the use of localStorage.
By using the BroadcastChannel API, you can enable basic communication between browsing contexts such as windows, tabs, frames, components, or iframes, and workers on the same origin.
According to the MDN documentation, receiving a message during initialization is not supported in the BroadcastChannel, but if you want to support that functionality, you may need to add extra option to atomWithBroadcast, such as local storage.
import { atom, SetStateAction } from 'jotai'
export function atomWithBroadcast<Value>(key: string, initialValue: Value) {
const baseAtom = atom(initialValue)
const listeners = new Set<(event: MessageEvent<any>) => void>()
const channel = new BroadcastChannel(key)
channel.onmessage = (event) => {
listeners.forEach((l) => l(event))
}
const broadcastAtom = atom(
(get) => get(baseAtom),
(get, set, update: { isEvent: boolean; value: SetStateAction<Value> }) => {
set(baseAtom, update.value)
if (!update.isEvent) {
channel.postMessage(get(baseAtom))
}
},
)
broadcastAtom.onMount = (setAtom) => {
const listener = (event: MessageEvent<any>) => {
setAtom({ isEvent: true, value: event.data })
}
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
const returnedAtom = atom(
(get) => get(broadcastAtom),
(_get, set, update: SetStateAction<Value>) => {
set(broadcastAtom, { isEvent: false, value: update })
},
)
return returnedAtom
}
const broadAtom = atomWithBroadcast('count', 0)
const ListOfThings = () => {
const [count, setCount] = useAtom(broadAtom)
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}Recipes Atom With Compare (`docs/recipes/atom-with-compare.mdx`)
title: atomWithCompare nav: 9.05 keywords: creators,compare
atomWithComparecreates atom that triggers updates when custom compare functionareEqual(prev, next)is false.
This can help you avoid unwanted re-renders by ignoring state changes that don't matter to your application.
Note: Jotai uses Object.is internally to compare values when changes occur. If areEqual(a, b) returns false, but Object.is(a, b) returns true, Jotai will not trigger an update.
import { atomWithReducer } from 'jotai/utils'
export function atomWithCompare<Value>(
initialValue: Value,
areEqual: (prev: Value, next: Value) => boolean,
) {
return atomWithReducer(initialValue, (prev: Value, next: Value) => {
if (areEqual(prev, next)) {
return prev
}
return next
})
}Here's how you'd use it to make an atom that ignores updates that are shallow-equal:
import { atomWithCompare } from 'XXX'
import { shallowEquals } from 'YYY'
import { CSSProperties } from 'react'
const styleAtom = atomWithCompare<CSSProperties>(
{ backgroundColor: 'blue' },
shallowEquals,
)In a component:
const StylePreview = () => {
const [styles, setStyles] = useAtom(styleAtom)
return (
<div>
<div styles={styles}>Style preview</div>
{/* Clicking this button twice will only trigger one render */}
<button onClick={() => setStyles({ ...styles, backgroundColor: 'red' })}>
Set background to red
</button>
{/* Clicking this button twice will only trigger one render */}
<button onClick={() => setStyles({ ...styles, fontSize: 32 })}>
Enlarge font
</button>
</div>
)
}Recipes Atom With Debounce (`docs/recipes/atom-with-debounce.mdx`)
title: atomWithDebounce nav: 9.10 keywords: creators,debounce
atomWithDebouncehelps with creating an atom where state set should be debounced.
This util is useful for text search inputs, where you would like to call functions in derived atoms only once after waiting for a duration, instead of firing an action on every keystroke.
/* Detailed source-code truncated for AI context efficiency. */Debouncing and async work
Clearing the timeout in clearTimeoutAtom cancels a scheduled publication to debouncedValueAtom, but it does not cancel async work that has already started.
For an async derived atom, pass its options.signal to fetch so the request can be aborted when that atom recalculates. If the atom reads only debouncedValueAtom, a new keystroke does not invalidate it immediately: the dependency changes when the debounced value is published (or a reset publishes it immediately). If a result should stop being eligible as soon as the text changes, track the current input separately from the debounced request value.
Caveat
Please note that this atom has different objectives from concurrent features in React 18 such as useTransition and useDeferredValue whose main aim is to prevent blocking of interaction with the page for expensive updates.
For more info, please read this github discussion https://github.com/reactwg/react-18/discussions/41 under the section titled "How is it different from setTimeout?"
Example Usage
The sandbox link below shows how we would use a derived atom to fetch state based on the value of debouncedValueAtom.
When typing a pokemon's name in <SearchInput>, we do not send a get request on every letter, but only after delayMilliseconds has passed since the last text input.
This reduces the number of backend requests to the server.
Recipes Atom With Listeners (`docs/recipes/atom-with-listeners.mdx`)
title: atomWithListeners nav: 9.08 keywords: creators,listeners
atomWithListenerscreates an atom and a hook. The hook can be called to
add a new listener. The hook takes as an argument a callback, and that
callback is called every time the atom's value is set. The hook also
returns a function to remove the listener.
This can be useful when you want to create a component that can listen to when
an atom's state changes without having to re-render that component with each of
those state changes.
import { useEffect } from 'react'
import {
atom,
useAtom,
useSetAtom,
Getter,
Setter,
SetStateAction,
} from 'jotai'
type Callback<Value> = (
get: Getter,
set: Setter,
newVal: Value,
prevVal: Value,
) => void
export function atomWithListeners<Value>(initialValue: Value) {
const baseAtom = atom(initialValue)
const listenersAtom = atom<Callback<Value>[]>([])
const anAtom = atom(
(get) => get(baseAtom),
(get, set, arg: SetStateAction<Value>) => {
const prevVal = get(baseAtom)
set(baseAtom, arg)
const newVal = get(baseAtom)
get(listenersAtom).forEach((callback) => {
callback(get, set, newVal, prevVal)
})
},
)
const useListener = (callback: Callback<Value>) => {
const setListeners = useSetAtom(listenersAtom)
useEffect(() => {
setListeners((prev) => [...prev, callback])
return () =>
setListeners((prev) => {
const index = prev.indexOf(callback)
return [...prev.slice(0, index), ...prev.slice(index + 1)]
})
}, [setListeners, callback])
}
return [anAtom, useListener] as const
}In a component:
const [countAtom, useCountListener] = atomWithListeners(0)
function EvenCounter() {
const [evenCount, setEvenCount] = useAtom(countAtom)
useCountListener(
useCallback(
(get, set, newVal, prevVal) => {
// Every time `countAtom`'s value is set, we check if its new value
// is even, and if it is, we increment `evenCount`.
if (newVal % 2 === 0) {
setEvenCount((c) => c + 1)
}
},
[setEvenCount],
),
)
return <>Count was set to an even number {evenCount} times.</>
}Recipes Atom With Refresh And Default (`docs/recipes/atom-with-refresh-and-default.mdx`)
title: atomWithRefreshAndDefault nav: 9.07 keywords: creators,refresh,default
This is for another implementation of atomWithDefault
Look back to atomWithDefault behavior
As you can see in the example code in atomWithDefault section, the two atoms' relation is disconnected after updating created one, count2Atom = atomWithDefault((get) => get(count1Atom) * 2).
Let's confirm what's occurred,
- Click "increment count1", then count1 is 2 and count2 is 4
- Click "increment count2", then count1 is 2 and count2 is 5 (Disconnected!!)
Those atoms have no relation after updating count2Atom. So,
- Click "increment count1", count1 is incremented only
- Even if you reset count2Atom, these dependency relation never come back
Motivation
In some cases,
- After disconnecting and resetting, they should come back to their relation
- Derived atoms should be reset based on updated the original atom
- We'd like to reset all derived atoms but just want to operate as simply as possible
How do we make those cases?
Here is a declarative way to create a function to provide a refreshable atom instead of atomWithDefault.
const refreshCountAtom = atom(0)
const baseDataAtom = atom(1) // original data, e.g. base count1Atom
const dataAtom = atom(
(get) => {
get(refreshCountAtom) // it's introduced at atomWithRefresh
return get(baseDataAtom)
},
(get, set, update) => {
set(baseDataAtom, update)
},
)
const atomWithRefreshAndDefault = (refreshAtom, getDefault) => {
const overwrittenAtom = atom(null)
return atom(
(get) => {
const lastState = get(overwrittenAtom)
if (lastState && lastState.refresh === get(refreshAtom)) {
return lastState.value
}
return getDefault(get)
},
(get, set, update) => {
set(overwrittenAtom, { refresh: get(refreshAtom), value: update })
},
)
}
// This is an alternative of `atomWithDefault((get) => get(count1Atom) * 2)`
const refreshableAtom = atomWithRefreshAndDefault(
refreshCountAtom,
(get) => get(dataAtom) * 2,
)
// You can reset by updating just one atom
const resetRootAtom = atom(null, (get, set) => {
set(refreshCountAtom, get(refreshCountAtom) + 1)
})Recipes Atom With Refresh (`docs/recipes/atom-with-refresh.mdx`)
title: atomWithRefresh nav: 9.06 keywords: creators,refresh
atomWithRefresh has been provided by jotai/utils since v2.7.0.
Jump to the doc
Recipes Atom With Toggle And Storage (`docs/recipes/atom-with-toggle-and-storage.mdx`)
title: atomWithToggleAndStorage nav: 9.05 keywords: creators,storage
atomWithToggleAndStorageis likeatomWithTogglebut also persist the state anytime it changes in given storage usingatomWithStorage.
Here is the source:
import { WritableAtom, atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export function atomWithToggleAndStorage(
key: string,
initialValue?: boolean,
storage?: any,
): WritableAtom<boolean, [boolean?], void> {
const anAtom = atomWithStorage(key, initialValue, storage)
const derivedAtom = atom(
(get) => get(anAtom),
(get, set, nextValue?: boolean) => {
const update = nextValue ?? !get(anAtom)
void set(anAtom, update)
},
)
return derivedAtom as WritableAtom<boolean, [boolean?], void>
}And how it's used:
import { atomWithToggleAndStorage } from 'XXX'
// will have an initial value set to false & get stored in localStorage under the key "isActive"
const isActiveAtom = atomWithToggleAndStorage('isActive')The usage in a component is also the same as atomWithToggle.
Recipes Atom With Toggle (`docs/recipes/atom-with-toggle.mdx`)
title: atomWithToggle nav: 9.04 keywords: creators,toggle
atomWithTogglecreates a new atom with a boolean as initial state & a setter function to toggle it.
This avoids the boilerplate of having to set up another atom just to update the state of the first.
import { WritableAtom, atom } from 'jotai'
export function atomWithToggle(
initialValue?: boolean,
): WritableAtom<boolean, [boolean?], void> {
const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => {
const update = nextValue ?? !get(anAtom)
set(anAtom, update)
})
return anAtom as WritableAtom<boolean, [boolean?], void>
}An optional initial state can be provided as the first argument.
The setter function can have an optional argument to force a particular state, such as if you want to make a setActive function out of it.
Here is how it's used.
import { atomWithToggle } from 'XXX'
// will have an initial value set to true
const isActiveAtom = atomWithToggle(true)And in a component:
const Toggle = () => {
const [isActive, toggle] = useAtom(isActiveAtom)
return (
<>
<button onClick={() => toggle()}>
isActive: {isActive ? 'yes' : 'no'}
</button>
<button onClick={() => toggle(true)}>force true</button>
<button onClick={() => toggle(false)}>force false</button>
</>
)
}Recipes Custom Useatom Hooks (`docs/recipes/custom-useatom-hooks.mdx`)
title: Custom useAtom hooks nav: 9.02 keywords: custom,hook
This page shows the ways of creating different utility functions. Utility functions save your time on coding, and you can preserve your base atom for other usage.
utils
useSelectAtom
import { useAtomValue } from 'jotai'
import { selectAtom } from 'jotai/utils'
export function useSelectAtom(anAtom, selector) {
const selectorAtom = selectAtom(
anAtom,
selector,
// Alternatively, you can customize `equalityFn` to determine when it will rerender
// Check selectAtom's signature for details.
)
return useAtomValue(selectorAtom)
}
// how to use it
function useN(n) {
const selector = useCallback((v) => v[n], [n])
return useSelectAtom(arrayAtom, selector)
}Please note that in this case keyFn must be stable, either define outside render or wrap with useCallback.
useFreezeAtom
import { useAtom } from 'jotai'
import { freezeAtom } from 'jotai/utils'
export function useFreezeAtom(anAtom) {
return useAtom(freezeAtom(anAtom))
}useSplitAtom
import { useAtom } from 'jotai'
import { splitAtom } from 'jotai/utils'
export function useSplitAtom(anAtom) {
return useAtom(splitAtom(anAtom))
}extensions
useFocusAtom
import { useAtom } from 'jotai'
import { focusAtom } from 'jotai-optics'
/* if an atom is created here, please use `useMemo(() => atom(initValue), [initValue])` instead. */
export function useFocusAtom(anAtom, keyFn) {
return useAtom(focusAtom(anAtom, keyFn))
}
// how to use it
useFocusAtom(anAtom) {
useMemo(() => atom(initValue), [initValue]),
useCallback((optic) => optic.prop('key'), [])
}Stackblitz
Please note that in this case keyFn must be stable, either define outside render or wrap with useCallback.
Recipes Large Objects (`docs/recipes/large-objects.mdx`)
title: Large objects nav: 9.01 keywords: large,object
The examples and descriptions below are based on this codesandbox, so it will give you a better understanding if you check it out along with these examples.
Sometimes we have nested data we need to store in atoms, and we may need to change that data at different levels, or we need to use part of that data without listening to all changes.
Consider this example:
const initialData = {
people: [
{
name: 'Luke Skywalker',
information: { height: 172 },
siblings: ['John Skywalker', 'Doe Skywalker'],
},
{
name: 'C-3PO',
information: { height: 167 },
siblings: ['John Doe', 'Doe John'],
},
],
films: [
{
title: 'A New Hope',
planets: ['Tatooine', 'Alderaan'],
},
{
title: 'The Empire Strikes Back',
planets: ['Hoth'],
},
],
info: {
tags: ['People', 'Films', 'Planets', 'Titles'],
},
}focusAtom
focusAtomcreates a new atom, based on the focus that you pass to it. jotai-optics
We use this utility to focus an atom and create an atom from a specific part of the data. For example we may need to consume the people property of the above data, Here's how we do it:
import { atom } from 'jotai'
import { focusAtom } from 'jotai-optics'
const dataAtom = atom(initialData)
const peopleAtom = focusAtom(dataAtom, (optic) => optic.prop('people'))focusAtom returns WritableAtom which means it's possible to change the peopleAtom data.
If we change the films property of the above data example, the peopleAtom won't cause a re-render, so that's one of the benefits of using focusAtom.
splitAtom
The
splitAtomutility is useful when you want to get an atom for each element in a list. jotai/utils
We use this utility for atoms that return arrays as their values. For example, the peopleAtom we made above returns the people property array, so we can return an atom for each item of that array. If the array atom is writable, splitAtom returned atoms are going to be writable, if the array atom is read-only, the returned atoms will be read-only too.
import { splitAtom } from 'jotai/utils'
const peopleAtomsAtom = splitAtom(peopleAtom)And this is how we use it in components.
const People = () => {
const [peopleAtoms] = useAtom(peopleAtomsAtom)
return (
<div>
{peopleAtoms.map((personAtom) => (
<Person personAtom={personAtom} key={`${personAtom}`} />
))}
</div>
)
}selectAtom
This function creates a derived atom whose value is a function of the original atom's value. jotai/utils
This utility is like focusAtom, but it always returns a read-only atom.
Assume we want to consume the info data, and its data is always unchangeable. We can make a read-only atom from it and select that created atom.
// first we create a derived atom based on initialData.info
const infoAtom = atom((get) => get(dataAtom).info)Then we use it in our component:
import { atom, useAtom } from 'jotai'
import { selectAtom, splitAtom } from 'jotai/utils'
const tagsSelector = (s) => s.tags
const Tags = () => {
const tagsAtom = selectAtom(infoAtom, tagsSelector)
const tagsAtomsAtom = splitAtom(tagsAtom)
const [tagAtoms] = useAtom(tagsAtomsAtom)
return (
<div>
{tagAtoms.map((tagAtom) => (
<Tag key={`${tagAtom}`} tagAtom={tagAtom} />
))}
</div>
)
}Recipes Use Atom Effect (`docs/recipes/use-atom-effect.mdx`)
title: useAtomEffect nav: 9.03 keywords: effect, atom effect, side effect, side-effect, sideeffect, hook, useAtomEffect
useAtomEffectruns side effects in response to changes in atoms or props using atomEffect.
The effectFn reruns whenever the atoms it depends on change or the effectFn itself changes. Be sure to memoize the effectFn if it's a function defined in the component.
β οΈ Note: always prefer to use a stable version of useMemo and useCallback to avoid extra atomEffect recomputations. You may rely on useMemo as a performance optimization, but not as a semantic guarantee. In the future, React may choose to βforgetβ some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.
import { useMemoOne as useStableMemo } from 'use-memo-one'
import { useAtomValue } from 'jotai/react'
import { atomEffect } from 'jotai-effect'
type EffectFn = Parameters<typeof atomEffect>[0]
export function useAtomEffect(effectFn: EffectFn) {
useAtomValue(useStableMemo(() => atomEffect(effectFn), [effectFn]))
}Example Usage
import { useCallbackOne as useStableCallback } from 'use-memo-one'
import { atom, useAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
import { useAtomEffect } from './useAtomEffect'
const channelSubscriptionAtomFamily = atomFamily<Channel>(
(channelId: string) => {
return atom(new Channel(channelId))
},
)
const messagesAtom = atom<Message[]>([])
function Messages({ channelId }: { channelId: string }) {
const [messages] = useAtom(messagesAtom)
useAtomEffect(
useStableCallback(
(get, set) => {
const channel = get(channelSubscriptionAtomFamily(channelId))
const unsubscribe = channel.subscribe((message) => {
set(messagesAtom, (prev) => [...prev, message])
})
return unsubscribe
},
[channelId],
),
)
return (
<>
<h1>You have {messages.length} messages</h1>
<hr />
{messages.map((message) => (
<div key={message.id}>{message.text}</div>
))}
</>
)
}Recipes Use Reducer Atom (`docs/recipes/use-reducer-atom.mdx`)
title: useReducerAtom nav: 9.11 keywords: reducer, hook, useReducerAtom
useReducerAtom is a custom hook to apply a reducer to a primitive atom.
It's useful to change the update behavior temporarily.
Also, consider atomWithReducer
for the atom-level solution.
import { useCallback } from 'react'
import { useAtom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
export function useReducerAtom<Value, Action>(
anAtom: PrimitiveAtom<Value>,
reducer: (v: Value, a: Action) => Value,
) {
const [state, setState] = useAtom(anAtom)
const dispatch = useCallback(
(action: Action) => setState((prev) => reducer(prev, action)),
[setState, reducer],
)
return [state, dispatch] as const
}Example Usage
import { atom } from 'jotai'
const countReducer = (prev, action) => {
if (action.type === 'inc') return prev + 1
if (action.type === 'dec') return prev - 1
throw new Error('unknown action type')
}
const countAtom = atom(0)
const Counter = () => {
const [count, dispatch] = useReducerAtom(countAtom, countReducer)
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'inc' })}>+1</button>
<button onClick={() => dispatch({ type: 'dec' })}>-1</button>
</div>
)
}Third Party Bunja (`docs/third-party/bunja.mdx`)
title: Bunja nav: 5.03 keywords: scope,di,raii,lifetime
Bunja is lightweight State Lifetime Manager.
It provides an RAII wrapper for jΕtai atoms.
See also:
install
npm install bunjaDefining a Bunja
You can define a bunja using the bunja function.
When you access the defined bunja with the useBunja hook, a bunja instance is created.
If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed.
If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the bunja.effect function.
import { bunja } from 'bunja'
import { useBunja } from 'bunja/react'
const countBunja = bunja(() => {
const countAtom = atom(0)
bunja.effect(() => {
console.log('mounted')
return () => console.log('unmounted')
})
return { countAtom }
})
function MyComponent() {
const { countAtom } = useBunja(countBunja)
const [count, setCount] = useAtom(countAtom)
// Your component logic here
}Seeding the first instance
If a bunja needs creation-time data, use bunja.withSeed.
A default seed is required when the bunja is declared, so callers may still omit the seed.
The seed is used only when a matching bunja instance is first created.
It is not part of the bunja instance identity.
If the matching instance already exists, later seeds are ignored.
const formBunja = bunja.withSeed({ title: '' }, (seed) => {
const titleAtom = atom(seed.title)
return { titleAtom }
})
useBunja({ bunja: formBunja, seed: { title: 'Draft' } })Defining a Bunja that relies on other Bunja
If you want to manage a state with a broad lifetime and another state with a narrower lifetime, you can create a (narrower) bunja that depends on a (broader) bunja.
For example, you can think of a bunja that holds the page state and another bunja that holds the modal state.
The page state lives longer than the modal state, and the modal state should exist from the moment the modal opens until it closes.
In such a case, you can write the following code.
const pageBunja = bunja(() => {
const pageStateAtom = atom({})
return { pageStateAtom }
})
const childBunja = bunja(() => {
const { pageStateAtom } = bunja.use(pageBunja)
const childStateAtom = atom((get) => ({
...get(pageStateAtom),
child: 'state',
}))
return { childStateAtom }
})
const modalBunja = bunja(() => {
const { pageStateAtom } = bunja.use(pageBunja)
const modalStateAtom = atom((get) => ({
...get(pageStateAtom),
modal: 'state',
}))
bunja.effect(() => {
console.log('modal opened')
return () => console.log('modal closed')
})
return { modalStateAtom }
})
function Page() {
const [modalOpen, setModalOpen] = useState(false)
return (
<>
<Child />
{modalOpen && <Modal />}
</>
)
}
function Child() {
const { childStateAtom } = useBunja(childBunja)
const childState = useAtomValue(childStateAtom)
// ...
}
function Modal() {
const { modalStateAtom } = useBunja(modalBunja)
const modalState = useAtomValue(modalStateAtom)
// ...
}Notice that pageBunja is not directly useBunja-ed.
When you useBunja either childBunja or modalBunja, since they depend on pageBunja, it has the same effect as if pageBunja were also useBunja-ed.
When the modal is unmounted, there are no longer any places using useBunja(modalBunja), so the instance of modalBunja is automatically destroyed.
Dependency injection using Scope
You can use a bunja for local state management.
When you specify a scope as a dependency of the bunja, separate bunja instances are created based on the values injected into the scope.
import { bunja, createScope } from 'bunja'
const UrlScope = createScope()
const fetchBunja = bunja(() => {
const url = bunja.use(UrlScope)
const queryAtom = atomWithQuery((get) => ({
queryKey: [url],
queryFn: async () => (await fetch(url)).json(),
}))
return { queryAtom }
})Injecting dependencies via React context
If you bind a scope to a React context, bunjas that depend on the scope can retrieve values from the corresponding React context.
In the example below, there are two React instances (<ChildComponent />) that reference the same fetchBunja, but since each looks at a different context value, two separate bunja instances are also created.
import { createContext } from 'react'
import { bunja, createScope } from 'bunja'
import { bindScope } from 'bunja/react'
const UrlContext = createContext('https://example.com/')
const UrlScope = createScope()
bindScope(UrlScope, UrlContext)
const fetchBunja = bunja(() => {
const url = bunja.use(UrlScope)
const queryAtom = atomWithQuery((get) => ({
queryKey: [url],
queryFn: async () => (await fetch(url)).json(),
}))
return { queryAtom }
})
function ParentComponent() {
return (
<>
<UrlContext value="https://example.com/foo">
<ChildComponent />
</UrlContext>
<UrlContext value="https://example.com/bar">
<ChildComponent />
</UrlContext>
</>
)
}
function ChildComponent() {
const { queryAtom } = useBunja(fetchBunja)
const { data, isPending, isError } = useAtomValue(queryAtom)
// Your component logic here
}You can use the createScopeFromContext function to handle both the creation of the scope and the binding to the context in one step.
import { createContext } from 'react'
import { createScopeFromContext } from 'bunja/react'
const UrlContext = createContext('https://example.com/')
const UrlScope = createScopeFromContext(UrlContext)Injecting dependencies directly into the scope
You might want to use a bunja directly within a React component where the values to be injected into the scope are created.
In such cases, you can use the second parameter of useBunja hook to inject values into the scope without wrapping the context separately.
function MyComponent() {
const { queryAtom } = useBunja(fetchBunja, [
UrlScope.bind('https://example.com/'),
])
const { data, isPending, isError } = useAtomValue(queryAtom)
// Your component logic here
}Doing the same thing inside a bunja
You can pass scope value pairs as the second argument to bunja.use to override scope values from within a bunja initialization function.
const myBunja = bunja(() => {
const fooData = bunja.use(fetchBunja, [
UrlScope.bind('https://example.com/foo'),
])
const barData = bunja.use(fetchBunja, [
UrlScope.bind('https://example.com/bar'),
])
return { fooData, barData }
})Third Party Eager (`docs/third-party/eager.mdx`)
title: Eager nav: 5.01 keywords: eager,derive,derived,async,zalgo,suspense,promise,react
JΕtai Eager provides primitives for building asynchronous data graphs that act on values as soon as they are available (either awaiting for them, or acting on them synchronously).
When is this useful?
- When local updates to the cache cause micro-suspensions
- When unnecessary recomputation causes performance issues
Install
npm install jotai-eagerMotivation
JΕtai offers powerful primitives for working with asynchronous data outside of the web framework (e.g. React), and allows the UI and business logic to properly integrate with the data layer. Many data-fetching integrations offer a peek into the client-side cache via atoms. When the cache is not yet populated, the atom has to resolve to a Promise of the value. However, if the value already exists in cache, and we do an optimistic update, then the value can be made available downstream immediately.
Building data graphs with these dual-natured (sometimes async, sometimes sync) atoms as a base can lead to unnecessary rerenders, stale values and micro-suspensions (in case of React) if not handled with care.
Eager atoms
Let's say we have an atom that fetches names of pets from an API, and a filter atom:
const petsAtom = atom<Promise<string[]>>(...);
const filterAtom = atom('cat');To create an atom of filtered pets using vanilla atoms, we would do the following:
const filteredPetsAtom = atom(async (get) => {
const filter = get(filterAtom)
const pets = await get(petsAtom)
return pets.filter((name) => name.includes(filter))
}) // => Atom<Promise<string[]>>filteredPetsAtom always returns a promise, even though the result
could be computed eagerly if the filterAtom was
the only changed dependency. We can fix that with jotai-eager:
import { eagerAtom } from 'jotai-eager'
const filteredPetsAtom = eagerAtom((get) => {
const filter = get(filterAtom)
const pets = get(petsAtom) // β¨ no await β¨
return pets.filter((name) => name.includes(filter))
}) // => Atom<Promise<string[]> | string[]>Now, the type reflects the eager behavior of this atom.
Its value will be string[] if the only thing that
changed is the filter, and Promise<string[]> otherwise!
Avoiding request waterfalls
If your atom has multiple async dependencies, it's best to initiate all of them simultaneously and wait for their results, instead of awaiting them sequentially. In vanilla async atoms, Promise.all(...) is the API to use, but in eager atoms, use the get.all() API:
const myMessages = eagerAtom((get) => {
const [user, messages] = get.all([userAtom, messagesAtom])
return messages.filter((msg) => msg.authorId === user.id)
}) // => Atom<Message[] | Promise<Message[]>>Awaiting a Promise that is not another atom's value
We can use the get.await API to await regular Promises inside eagerAtom definitions, as long as we make sure that the Promise
we're passing is consistent across invocations of the atom's read function.
const statusAtom = eagerAtom((get) => {
const statusPromise = get(currentInvoiceAtom).getStatus() // => Promise<InvoiceStatus>
const status = get.await(statusPromise)
// ^? InvoiceStatus
return status
})Handling Loading States with `loadable`
The loadable API wraps an atom to provide a consistent loading state representation, sharing a Promise cache between all jotai-eager APIs to minimize suspensions.
import { atom } from 'jotai';
import { loadable } from 'jotai-eager';
const asyncAtom = atom(async () => 'data');
const loadableAtom = loadable(asyncAtom);
// Use in component:
const state = useAtom(loadableAtom);
if (state.state === 'loading') return <div>Loading...</div>;
if (state.state === 'hasError') return <div>Error: {state.error}</div>;
return <div>{state.data}</div>;Handling Pending States with `withPending`
The withPending API wraps an atom to handle unresolved values by returning a fallback, providing an alternative to Jotai's unwrap with enhanced pending state management.
import { atom } from 'jotai'
import { withPending } from 'jotai-eager'
const asyncAtom = atom(Promise.resolve('data'))
const wrappedAtom = withPending(asyncAtom, () => 'Loading...')
// Returns 'Loading...' while pending, then 'data'Using `try` & `catch` inside eager atoms
Eager atoms internally use exceptions to suspend computation of the atom until an async dependency is fulfilled (similar to React's suspense behavior, but does not require React to function). This means that using exception handling inside eager atoms has to be instrumented with an additional call to isEagerError.
import { eagerAtom, isEagerError } from 'jotai-eager'
const fooAtom = eagerAtom((get) => {
try {
// ...
} catch (e) {
if (isEagerError(e)) {
// Rethrow the error to be handled by `jotai-eager`
throw e
}
// ...
}
})Awaiting a Promise that is created inside the atom
Since the read function is 'retried' after a Promise we await is fulfilled, the mechanism expects
the same promise to be passed into get.await the second time around. Since we are creating the
Promise inside of the read function itself, that will never be the case, and we'll be stuck in an infinite loop.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
// This atom will be stuck in an infinite loop :(
const deferredNumberAtom = eagerAtom((get) => {
get.await(sleep(1000)) // Waiting for a second...
return 123
})For this particular use case, since we're always deferring, using an eagerAtom over
a vanilla async atom is unnecessary.
soon / soonAll
For more advanced usage, for example conditional dependencies, the soon and soonAll functions
can be used to perform sync/async transformations on data eagerly, on a more fine-grained level.
Conditional dependency
import { soon } from 'jotai-eager'
// Given the following definitions:
// const queryAtom: Atom<RestrictedItem | Promise<RestrictedItem>>;
// const isAdminAtom: Atom<boolean | Promise<boolean>>;
// Atom<RestrictedItem | null | Promise<RestrictedItem | null>>
const restrictedItemAtom = atom((get) => {
const isAdmin = get(isAdminAtom)
return soon(isAdmin, (isAdmin) => (isAdmin ? get(queryAtom) : null))
})Conditional dependency (multiple conditions)
import { soon, soonAll } from 'jotai-eager'
// Given the following definitions:
// const queryAtom: Atom<RestrictedItem | Promise<RestrictedItem>>;
// const isAdminAtom: Atom<boolean | Promise<boolean>>;
// const enabledAtom: Atom<boolean | Promise<boolean>>;
// Atom<RestrictedItem | null | Promise<RestrictedItem | null>>
const restrictedItemAtom = atom((get) => {
return soon(
soonAll(get(isAdminAtom), get(enabledAtom)),
([isAdmin, enabled]) => (isAdmin && enabled ? get(queryAtom) : null),
)
})Third Party History (`docs/third-party/history.mdx`)
title: History nav: 4.04 keywords: history, undo, redo, track changes
jotai-history is a utility package for tracking state history in Jotai.
Installation
npm install jotai-history`withHistory`
import { withHistory } from 'jotai-history'
const targetAtom = atom(0)
const limit = 2
const historyAtom = withHistory(targetAtom, limit)
function Component() {
const [current, previous] = useAtomValue(historyAtom)
...
}Description
withHistory creates an atom that tracks the history of states for a given targetAtom. The most recent limit states are retained.
Action Symbols
RESET
Clears the entire history, removing all previous states (including the undo/redo stack).jsimport { RESET } from 'jotai-history' ... function Component() { const setHistoryAtom = useSetAtom(historyAtom) ... setHistoryAtom(RESET) }UNDO and REDO
Moves thetargetAtombackward or forward in its history.jsimport { REDO, UNDO } from 'jotai-history' ... function Component() { const setHistoryAtom = useSetAtom(historyAtom) ... setHistoryAtom(UNDO) setHistoryAtom(REDO) }
Indicators
canUndo and canRedo
Booleans indicating whether undo or redo actions are currently possible. These can be used to disable buttons or conditionally trigger actions.jsx... function Component() { const history = useAtomValue(historyAtom) return ( <> <button disabled={!history.canUndo}>Undo</button> <button disabled={!history.canRedo}>Redo</button> </> ) }
Memory Management
Because
withHistorymaintains a list of previous states, be mindful of memory usage by setting a reasonablelimit. Applications that update state frequently can grow significantly in memory usage.
Tools Babel (`docs/tools/babel.mdx`)
title: Babel nav: 6.02 keywords: babel,gatsby,fast,refresh
Deprecated:
jotai/babelbundle is deprecated and will be removed in v3. Please use thejotai-babelpackage instead.
jotai-babel/plugin-react-refresh
This plugin adds support for React Refresh for Jotai atoms. This makes sure that state isn't reset, when developing using React Refresh.
Usage
With a babel configuration file:
{
"plugins": ["jotai-babel/plugin-react-refresh"]
}The plugin recognizes a predefined list of atom function names (e.g. 'atom', 'atomFamily', 'atomWithDefault'). If you're using custom atom function names, you can explicitly supply them to ensure they are recognized.
Here's how you can configure it in your Babel setup:
{
"plugins": [
["jotai-babel/plugin-react-refresh", { "customAtomNames": ["customAtom"] }]
]
}Examples can be found below.
jotai-babel/plugin-debug-label
Jotai is based on object references and not keys (like Recoil). This means there's no identifier for atoms. To identify atoms, it's possible to add a debugLabel to an atom, which can be found in React devtools.
However, this can quickly become cumbersome to add a debugLabel to every atom.
This babel plugin adds a debugLabel to every atom, based on its identifier.
The plugin transforms this code:
export const countAtom = atom(0)Into:
export const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'Default exports are also handled, based on the file naming:
// countAtom.ts
export default atom(0)Which transform into:
// countAtom.ts
const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'
export default countAtomUsage
It is recommended to disable this plugin for production builds to avoid unnecessary overhead. You can conditionally include it in your Babel configuration based on the environment.
With a babel configuration file:
{
"plugins": ["jotai-babel/plugin-debug-label"]
}The plugin recognizes a predefined list of atom function names (e.g. 'atom', 'atomFamily', 'atomWithDefault'). If you're using custom atom function names, you can explicitly supply them to ensure they are recognized.
Here's how you can configure it in your Babel setup:
{
"plugins": [
["jotai-babel/plugin-debug-label", { "customAtomNames": ["customAtom"] }]
]
}Examples can be found below.
jotai-babel/preset
Jotai ships with a babel preset containing the plugins created for Jotai.
Usage
With a babel configuration file:
{
"presets": ["jotai-babel/preset"]
}You can also supply your atom names to the preset:
{
"presets": [["jotai-babel/preset", { "customAtomNames": ["customAtom"] }]]
}Examples
Next.js
Parcel
Tools Devtools (`docs/tools/devtools.mdx`)
title: Devtools nav: 6.04 keywords: devtools,debug,snapshot
Install
Install jotai-devtools to your project to get started.
npm install jotai-devtoolsNotes
<DevTools/>is optimized to be tree-shakable for production builds, and only works in a non-production environment- Hooks are dev-only, and are designed to work in a non-production environment
- Feedback welcome, please file issues or ask questions on the Jotai DevTools GitHub Repo
Quick links
- UI Devtools
- Hooks
- Migration guides
UI DevTools
Enhance your development experience with the UI based Jotai DevTool.
Babel plugin setup - (_Optional but highly recommended_)
Use jotai-babel plugins for optimal debugging experience. Find the complete guide on the babel page and/or swc page.
npm install -D jotai-babel{
"presets": [
// The preset includes two plugins:
// - jotai-babel/plugin-react-refresh to enable hot reload for atoms
// - jotai-babel/plugin-debug-label to automatically adds debug labels to atoms
'jotai-babel/preset'
]
}Example for Vite + React project:
// vite.config.ts
export default defineConfig({
plugins: [
react({
babel: {
presets: ['jotai-babel/preset'],
},
}),
],
})If you are using Vite 8, first install these additional packages:
npm install -D @rolldown/plugin-babel @babel/coreThen use the following example instead:
// vite.config.ts
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({
presets: ['jotai-babel/preset'],
})
],
})Next JS setup
You may skip this section if you're not using Next.js.
Enable transpilePackages for the UI CSS and components to be transpiled correctly.
// next.config.ts
const nextConfig = {
// Learn more here - https://nextjs.org/docs/advanced-features/compiler#module-transpilation
// Required for UI css to be transpiled correctly π
transpilePackages: ['jotai-devtools'],
}
module.exports = nextConfigAvailable props
type DevToolsProps = {
// defaults to false
isInitialOpen?: boolean
// pass a custom store
store?: Store
// Defaults to light
theme?: 'dark' | 'light'
// Set the position of the trigger button
// Defaults to `bottom-left`
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
// Custom nonce to allowlist jotai-devtools specific inline styles via CSP
nonce?: string
options?: {
// Private atoms are used internally in atom creators like `atomWithStorage`
// or `atomWithLocation`, etc. to manage the internal state.
// Defaults to `false`
shouldShowPrivateAtoms?: boolean
// Expands the JSON tree view on initial render on Atom Viewer tab, Timeline tab, etc.
// Defaults to `false`
shouldExpandJsonTreeViewInitially?: boolean
// The interval (in milliseconds) between each step of the time travel playback.
// Defaults to `750ms`
timeTravelPlaybackInterval?: number
// The maximum number of snapshots to keep in the history.
// The higher the number the more memory it will consume.
// Defaults to `Infinity`. Recommended: `~30`
snapshotHistoryLimit?: number
}
}Usage
Provider-less
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
const App = () => {
return (
<>
<DevTools />
{/* your app */}
</>
)
}With Provider
import { createStore } from 'jotai'
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
const customStore = createStore()
const App = () => {
return (
<Provider store={customStore}>
<DevTools store={customStore} />
{/* your app */}
</Provider>
)
}Preview
useAtomsDebugValue
useAtomsDebugValue is a React hook that will show all atom values in React Devtools.
function useAtomsDebugValue(options?: {
store?: Store
enabled?: boolean
}): voidInternally, it uses useDebugValue which is only effective in DEV mode.
It will catch all atoms that are accessible from the place the hook is located.
Example
import { useAtomsDebugValue } from 'jotai-devtools/utils'
const textAtom = atom('hello')
textAtom.debugLabel = 'textAtom'
const lenAtom = atom((get) => get(textAtom).length)
lenAtom.debugLabel = 'lenAtom'
const TextBox = () => {
const [text, setText] = useAtom(textAtom)
const [len] = useAtom(lenAtom)
return (
<span>
<input value={text} onChange={(e) => setText(e.target.value)} />({len})
</span>
)
}
const DebugAtoms = () => {
useAtomsDebugValue()
return null
}
const App = () => (
<Provider>
<DebugAtoms />
<TextBox />
</Provider>
)useAtomDevtools
useAtomDevtools is a React hook that manages ReduxDevTools extension for a particular atom.
function useAtomDevtools<Value>(
anAtom: WritableAtom<Value, Value>,
options?: {
store?: Store
name?: string
enabled?: boolean
},
): voidThe useAtomDevtools hook accepts a generic type parameter (mirroring the type stored in the atom). Additionally, the hook accepts two invocation parameters, anAtom and name.anAtom is the atom that will be attached to the devtools instance. name is an optional parameter that defines the debug label for the devtools instance. If name is undefined, atom.debugLabel will be used instead.
Example
import { useAtomDevtools } from 'jotai-devtools/utils'
// The interface for the type stored in the atom.
export interface Task {
label: string
complete: boolean
}
// The atom to debug.
export const tasksAtom = atom<Task[]>([])
// If the useAtomDevtools name parameter is undefined, this value will be used instead.
tasksAtom.debugLabel = 'Tasks'
export const useTasksDevtools = () => {
// The hook can be called simply by passing an atom for debugging.
useAtomDevtools(tasksAtom)
// Specify a custom type parameter
useAtomDevtools<Task[]>(tasksAtom)
// You can attach two devtools instances to the same atom and differentiate them with custom names.
useAtomDevtools(tasksAtom, 'Tasks (Instance 1)')
useAtomDevtools(tasksAtom, 'Tasks (Instance 2)')
}useAtomsDevtools
β οΈ Note: This hook is experimental (feedbacks are welcome) and only works in a process.env.NODE_ENV !== 'production' environment.
useAtomsDevtools is a catch-all version of useAtomDevtools where it shows all atoms in the store instead of showing a specific one.
function useAtomsDevtools(
name: string,
options?: {
store?: Store
enabled?: boolean
},
): voidIt takes a name parameter that is needed for naming the Redux devtools instance and a store parameter.
As a limitation for this API, we need to put useAtomsDevtools in a component where the consumed atoms should be in a lower place of the React tree than that component (AtomsDevtools in the below example).AtomsDevtools component can be considered as a best practice for our apps.
Example
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
...
}
const AtomsDevtools = ({ children }) => {
useAtomsDevtools('demo')
return children
}
export default function App() {
return (
<AtomsDevtools>
<Counter />
</AtomsDevtools>
)
}useAtomsSnapshot
β οΈ Note: This hook only works in a process.env.NODE_ENV !== 'production' environment. And it returns a static empty value in production.
useAtomsSnapshot takes a snapshot of the currently mounted atoms and their state.
function useAtomsSnapshot(options?: { store?: Store }): AtomsSnapshotIt accepts a store parameter and will return an AtomsSnapshot, which is basically a Map<AnyAtom, unknown>. You can use the Map API to iterate over atoms and their state.
This hook is primarily meant for debugging and devtools use cases.
Be careful using this hook because it will cause the component to re-render for all state changes.
Example
import { Provider } from 'jotai'
import { useAtomsSnapshot } from 'jotai-devtools/utils'
const RegisteredAtoms = () => {
const atoms = useAtomsSnapshot()
return (
<div>
<p>Atom count: {atoms.size}</p>
<div>
{Array.from(atoms).map(([atom, atomValue]) => (
<p key={`${atom}`}>{`${atom.debugLabel}: ${atomValue}`}</p>
))}
</div>
</div>
)
}
const App = () => (
<Provider>
<RegisteredAtoms />
</Provider>
)useGotoAtomsSnapshot
β οΈ Note: This hook only works in a process.env.NODE_ENV !== 'production' environment. And it works like an empty function in the production environment.
useGotoAtomsSnapshot will update the current Jotai state to match the passed snapshot.
function useGotoAtomsSnapshot(options?: {
store?: Store
}): (values: Iterable<readonly [AnyAtom, unknown]>) => voidThis hook returns a callback, which takes a snapshot from the useAtomsSnapshot hook and will update the Jotai state. It accepts a store parameter.
This hook is primarily meant for debugging and devtools use cases.
Example
import { Provider } from 'jotai'
import { useAtomsSnapshot, useGotoAtomsSnapshot } from 'jotai-devtools/utils'
const petAtom = atom('cat')
const colorAtom = atom('blue')
const UpdateSnapshot = () => {
const snapshot = useAtomsSnapshot()
const goToSnapshot = useGotoAtomsSnapshot()
return (
<button
onClick={() => {
const newSnapshot = new Map(snapshot)
newSnapshot.set(petAtom, 'dog')
newSnapshot.set(colorAtom, 'green')
goToSnapshot(newSnapshot)
}}
>
Go to snapshot
</button>
)
}Tools Rolldown (`docs/tools/rolldown.mdx`)
title: Rolldown nav: 6.03 keywords: rolldown,vite,fast,refresh
β οΈ Note: This plugin is experimental. Feedback is welcome in the Github repo. Please file issues in the separate repo instead of jotai repo.
jotaijs/jotai-rolldown
This plugin improves the development experience for Jotai by adding React Refresh support and adding devtools support.
Usage
Install it with:
npm install --save-dev jotai-rolldownYou can add the plugin to rolldown.config.ts:
import jotai from 'jotai-rolldown'
import { defineConfig } from 'rolldown'
export default defineConfig({
plugins: [jotai()],
})or you can add the plugin to vite.config.ts:
import jotai from 'jotai-rolldown'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [jotai()],
})Options
`reactRefresh`
Type: boolean
Default: true (in development)
Enables React Refresh support.
This makes sure that state isn't reset, when developing using React Refresh.
`debugLabel`
Type: boolean
Default: true (in development)
Jotai is based on object references and not keys (like Recoil). This means there's no identifier for atoms. To identify atoms, it's possible to add a debugLabel to an atom, which can be found in React devtools.
However, this can quickly become cumbersome to add a debugLabel to every atom.
This plugin adds a debugLabel to every atom, based on its identifier.
The plugin transforms this code:
export const countAtom = atom(0)Into:
export const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'Default exports are also handled, based on the file naming:
// countAtom.ts
export default atom(0)Which transform into:
// countAtom.ts
const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'
export default countAtom`atomNames`
Type: string[]
Default: []
Custom atom function names so that the plugins can recognize them.
Tools Skill (`docs/tools/skill.mdx`)
title: Skill nav: 6.05 keywords: ai,agent,codex,claude,skill,skills
The skill helps coding agents write and review atom-based code. It can guide
atom design, derived atoms, write-only action atoms, async atoms, Provider/store
usage, render performance, TypeScript, tests, utilities, and extensions.
β οΈ Note: Feedback is welcome in the Github repo. Please file issues in the separate repo instead of jotai repo.
Install
Install it with:
npx skills add jotaijs/jotai-skills --skill jotaiInstall it to a specific coding agent:
npx skills add jotaijs/jotai-skills --skill jotai --agent codex
npx skills add jotaijs/jotai-skills --skill jotai --agent claude-codeInstall it globally:
npx skills add jotaijs/jotai-skills --skill jotai --globalUsage
After installing, ask a Jotai-related question:
Review the form atom design.Refactor React Context state into atoms.Reduce unrelated atom rerenders.Tools Swc (`docs/tools/swc.mdx`)
title: SWC nav: 6.01 keywords: swc,next,nextjs,fast,refresh
β οΈ Note: These plugins are experimental. Feedback is welcome in the Github repo. Please file issues in the separate repo instead of jotai repo.
@swc-jotai/react-refresh
This plugin adds support for React Refresh for Jotai atoms. This makes sure that state isn't reset, when developing using React Refresh.
Usage
Install it with:
npm install --save-dev @swc-jotai/react-refreshYou can add the plugin to .swcrc:
{
"jsc": {
"experimental": {
"plugins": [["@swc-jotai/react-refresh", {}]]
}
}
}You can use the plugin with the experimental SWC plugins feature in Next.js.
module.exports = {
experimental: {
swcPlugins: [['@swc-jotai/react-refresh', {}]],
},
}Examples can be found below.
@swc-jotai/debug-label
Jotai is based on object references and not keys (like Recoil). This means there's no identifier for atoms. To identify atoms, it's possible to add a debugLabel to an atom, which can be found in React devtools.
However, this can quickly become cumbersome to add a debugLabel to every atom.
This SWC plugin adds a debugLabel to every atom, based on its identifier.
The plugin transforms this code:
export const countAtom = atom(0)Into:
export const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'Default exports are also handled, based on the file naming:
// countAtom.ts
export default atom(0)Which transform into:
// countAtom.ts
const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'
export default countAtomUsage
Install it with:
npm install --save-dev @swc-jotai/debug-labelYou can add the plugin to .swcrc:
{
"jsc": {
"experimental": {
"plugins": [["@swc-jotai/debug-label", {}]]
}
}
}Or you can use the plugin with the experimental SWC plugins feature in Next.js.
module.exports = {
experimental: {
swcPlugins: [['@swc-jotai/debug-label', {}]],
},
}Examples can be found below.
Custom atom names
You can enable the plugins for your custom atoms. You can supply them to the plugins like below:
module.exports = {
experimental: {
swcPlugins: [
['@swc-jotai/debug-label', { atomNames: ['customAtom'] }],
['@swc-jotai/react-refresh', { atomNames: ['customAtom'] }],
],
},
}Examples
Next.js
Utilities Async (`docs/utilities/async.mdx`)
title: Async nav: 3.03 keywords: load,loadable,observable
All atoms support async behavior such as async read or async write. However there are APIs for more control described here.
loadable
In Jotai v3,
loadableis no longer part of the core package.
See the v3 migration guide
for a userland replacement built onunwrap.
Alternatively,useAtomValueRawanduseAtomValueRawSynclet you
read an async atom's value without suspending.
If you don't want async atoms to suspend or throw to an error boundary (for example, for finer-grained control of loading and error logic), you can use the loadable util.
It would work the same way for any atom. Simply wrap your atoms with the loadable util. It returns a value with one of three states: loading, hasData and hasError.
{
state: 'loading' | 'hasData' | 'hasError',
data?: any,
error?: any,
}import { loadable } from "jotai/utils"
const asyncAtom = atom(async (get) => ...)
const loadableAtom = loadable(asyncAtom)
// Does not need to be wrapped by a <Suspense> element
const Component = () => {
const [value] = useAtom(loadableAtom)
if (value.state === 'hasError') return <Text>{value.error}</Text>
if (value.state === 'loading') {
return <Text>Loading...</Text>
}
console.log(value.data) // Results of the Promise
return <Text>Value: {value.data}</Text>
}atomWithObservable
Ref: https://github.com/pmndrs/jotai/pull/341
Usage
import { useAtom } from 'jotai'
import { atomWithObservable } from 'jotai/utils'
import { interval } from 'rxjs'
import { map } from 'rxjs/operators'
const counterSubject = interval(1000).pipe(map((i) => `#${i}`))
const counterAtom = atomWithObservable(() => counterSubject)
const Counter = () => {
const [counter] = useAtom(counterAtom)
return <div>count: {counter}</div>
}The atomWithObservable function creates an atom from a rxjs (or similar) subject or observable.
Its value will be last value emitted from the stream.
To use this atom, you need to wrap your component with <Suspense>. Check out guides/async.
Initial value
atomWithObservable takes second optional parameter { initialValue } that allows to specify initial value for the atom. If initialValue is provided then atomWithObservable will not suspend and will show initial value before receiving first value from observable. initialValue can be either a value or a function that returns a value
const counterAtom = atomWithObservable(() => counterSubject, {
initialValue: 10,
})
const counterAtom2 = atomWithObservable(() => counterSubject, {
initialValue: () => Math.random(),
})Stackblitz
unwrap
The unwrap util will convert an async atom to a sync atom like loadable.
Unlike loadable, the fallback value can be configured.
Unlike loadable, the error won't be handled and just thrown.
The use case of unwrap is to ease deriving atoms.
This is especially useful for v2 API,
because get in the read function doesn't resolve promises.
Signature
function unwrap<Value, Args extends unknown[], Result>(
anAtom: WritableAtom<Value, Args, Result>,
): WritableAtom<Awaited<Value> | undefined, Args, Result>
function unwrap<Value, Args extends unknown[], Result, PendingValue>(
anAtom: WritableAtom<Value, Args, Result>,
fallback: (prev?: Awaited<Value>) => PendingValue,
): WritableAtom<Awaited<Value> | PendingValue, Args, Result>
function unwrap<Value>(anAtom: Atom<Value>): Atom<Awaited<Value> | undefined>
function unwrap<Value, PendingValue>(
anAtom: Atom<Value>,
fallback: (prev?: Awaited<Value>) => PendingValue,
): Atom<Awaited<Value> | PendingValue>Usage
import { atom } from 'jotai'
import { unwrap } from 'jotai/utils'
const countAtom = atom(0)
const delayedCountAtom = atom(async (get) => {
await new Promise((r) => setTimeout(r, 500))
return get(countAtom)
})
const unwrapped1Atom = unwrap(delayedCountAtom)
// The value is `undefined` while pending
const unwrapped2Atom = unwrap(delayedCountAtom, (prev) => prev ?? 0)
// The value is `0` initially, and subsequent updates keep the previous value.Utilities Callback (`docs/utilities/callback.mdx`)
title: Callback nav: 3.99 keywords: callback published: false
useAtomCallback
Ref: https://github.com/pmndrs/jotai/issues/60
Usage
useAtomCallback<Result, Args extends unknown[]>(
callback: (get: Getter, set: Setter, ...arg: Args) => Result,
options?: Options
): (...args: Args) => ResultThis hook is for interacting with atoms imperatively.
It takes a callback function that works like atom write function,
and returns a function that returns an atom value.
The callback to pass in the hook must be stable (should be wrapped with useCallback).
Examples
import { useEffect, useState, useCallback } from 'react'
import { Provider, atom, useAtom } from 'jotai'
import { useAtomCallback } from 'jotai/utils'
const countAtom = atom(0)
const Counter = () => {
const [count, setCount] = useAtom(countAtom)
return (
<>
{count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</>
)
}
const Monitor = () => {
const [count, setCount] = useState(0)
const readCount = useAtomCallback(
useCallback((get) => {
const currCount = get(countAtom)
setCount(currCount)
return currCount
}, []),
)
useEffect(() => {
const timer = setInterval(async () => {
console.log(readCount())
}, 1000)
return () => {
clearInterval(timer)
}
}, [readCount])
return <div>current count: {count}</div>
}Stackblitz
Utilities Family (`docs/utilities/family.mdx`)
title: Family nav: 3.5 keywords: family
atomFamily
:::caution DeprecatedatomFamily is deprecated and will be removed in Jotai v3.
Please migrate to the jotai-family package, which provides the same API with additional features like atomTree.
Migration:
npm install jotai-family// Before
import { atomFamily } from 'jotai/utils'
// After
import { atomFamily } from 'jotai-family'The API is identical, so no code changes are needed beyond the import statement.
:::
Ref: https://github.com/pmndrs/jotai/issues/23
Usage
atomFamily(initializeAtom, areEqual): (param) => AtomThis will create a function that takes param and returns an atom.
If the atom has already been created, it will be returned from the cache.initializeAtom is a function that can return any kind of atom (atom(), atomWithDefault(), ...).
Note that the areEqual argument is optional and compares
if two params are equal (defaults to Object.is).
To reproduce behavior similar to Recoil's atomFamily/selectorFamily,
specify a deepEqual function to areEqual. For example:
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
import deepEqual from 'fast-deep-equal'
const fooFamily = atomFamily((param) => atom(param), deepEqual)TypeScript
The atom family types will be inferred from initializeAtom. Here's a typical usage with a primitive atom.
import type { PrimitiveAtom } from 'jotai'
/**
* here the atom(id) returns a PrimitiveAtom<number>
* and PrimitiveAtom<number> is a WritableAtom<number, SetStateAction<number>>
*/
const myFamily = atomFamily((id: number) => atom(id))You can explicitly declare the type of parameter and atom type using TypeScript generics.
atomFamily<Param, AtomType extends Atom<unknown>>(
initializeAtom: (param: Param) => AtomType,
areEqual?: (a: Param, b: Param) => boolean
): AtomFamily<Param, AtomType>Example with explicit types:
import { atom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
const myFamily = atomFamily<number, PrimitiveAtom<number>>((id: number) =>
atom(id),
)API Methods
The atomFamily function returns an object with the following methods:
`myFamily(param)`
Returns an atom for the given param. If the atom has already been created, it will be returned from the cache.
`myFamily.getParams()`
Returns an iterable of all params currently in the cache.
const todoFamily = atomFamily((name) => atom(name))
todoFamily('foo')
todoFamily('bar')
for (const param of todoFamily.getParams()) {
console.log(param) // 'foo', 'bar'
}`myFamily.remove(param)`
Removes a specific param from the cache.
todoFamily.remove('foo')`myFamily.setShouldRemove(shouldRemove)`
Registers a shouldRemove function which runs immediately and when you are about to get an atom from the cache.
shouldRemoveis a function that takes two arguments:createdAt(in milliseconds) andparam, and returns a boolean value.- Setting
nullwill remove the previously registered function.
// Remove atoms older than 1 hour
todoFamily.setShouldRemove((createdAt, param) => {
return Date.now() - createdAt > 60 * 60 * 1000
})`myFamily.unstable_listen(callback)`
β οΈ Unstable API: This API is for advanced use cases and can change without notice.
Fires when an atom is created or removed. Returns a cleanup function.
const cleanup = todoFamily.unstable_listen((event) => {
console.log(event.type) // 'CREATE' or 'REMOVE'
console.log(event.param) // the param
console.log(event.atom) // the atom
})
// Later, stop listening
cleanup()Caveat: Memory Leaks
Internally, atomFamily is just a Map whose key is a param and whose value is an atom config.
Unless you explicitly remove unused params, this leads to memory leaks.
This is crucial if you use infinite number of params.
Use myFamily.remove(param) or myFamily.setShouldRemove(shouldRemove) to manage memory.
Examples
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
const todoFamily = atomFamily((name) => atom(name))
todoFamily('foo')
// this will create a new atom('foo'), or return the one if already createdimport { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
const todoFamily = atomFamily((name) =>
atom(
(get) => get(todosAtom)[name],
(get, set, arg) => {
const prev = get(todosAtom)
set(todosAtom, { ...prev, [name]: { ...prev[name], ...arg } })
},
),
)import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
const todoFamily = atomFamily(
({ id, name }) => atom({ name }),
(a, b) => a.id === b.id,
)Stackblitz
Migration to jotai-family
For new projects or when updating existing code, we recommend using the jotai-family package instead. It provides:
- Same API: Drop-in replacement for
atomFamily - Additional features: Includes
atomTreefor hierarchical atom management - Better maintenance: Dedicated package with focused development
- Future-proof: Will continue to be supported in Jotai v3 and beyond
See the jotai-family documentation for more details.
Utilities Lazy (`docs/utilities/lazy.mdx`)
title: Lazy nav: 3.03 keywords: lazy,initialize,init,loading
When defining primitive atoms, their initial value has to be bound at definition time.
If creating that initial value is computationally expensive, or the value is not accessible during definition,
it would be best to postpone the atom's initialization until its first use in the store.
const imageDataAtom = atom(initializeExpensiveImage()) // 1) has to be computed here
function Home() {
...
}
function ImageEditor() {
// 2) used only in this route
const [imageData, setImageData] = useAtom(imageDataAtom);
...
}
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/edit" component={ImageEditor} />
</Router>
)
}atomWithLazy
Ref: https://github.com/pmndrs/jotai/pull/2465
We can use atomWithLazy to create a primitive atom whose initial value will be computed at first use in the store.
After initialization, it will behave like a regular primitive atom (can be written to).
Usage
import { atomWithLazy } from 'jotai/utils'
// passing the initialization function
const imageDataAtom = atomWithLazy(initializeExpensiveImage)
function Home() {
...
}
function ImageEditor() {
// only gets initialized if user goes to `/edit`.
const [imageData, setImageData] = useAtom(imageDataAtom);
...
}
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/edit" component={ImageEditor} />
</Router>
)
}Using multiple stores
Since each store is its separate universe, the initial value will be recreated exactly once per store (unless using something like jotai-scope, which fractures a store into smaller universes).
type RGB = [number, number, number];
function randomRGB(): RGB {
...
}
const lift = (value: number) => ([r, g, b]: RGB) => {
return [r + value, g + value, b + value]
}
const colorAtom = lazyAtom(randomRGB)
let store = createStore()
console.log(store.get(colorAtom)) // [0, 36, 128]
store.set(colorAtom, lift(8))
console.log(store.get(colorAtom)) // [8, 44, 136]
// recreating store, sometimes done when logging out or resetting the app in some way
store = createStore()
console.log(store.get(colorAtom)) // [255, 12, 46] -- a new random colorUtilities Reducer (`docs/utilities/reducer.mdx`)
title: Reducer nav: 3.99 keywords: reducer,action,dispatch published: false
atomWithReducer
Ref: https://github.com/pmndrs/jotai/issues/38
import { atomWithReducer } from 'jotai/utils'
const countReducer = (prev, action) => {
if (action.type === 'inc') return prev + 1
if (action.type === 'dec') return prev - 1
throw new Error('unknown action type')
}
const countReducerAtom = atomWithReducer(0, countReducer)Stackblitz
useReducerAtom
See useReducerAtom recipe.
Utilities Resettable (`docs/utilities/resettable.mdx`)
title: Resettable nav: 3.04 keywords: reset,default
atomWithReset
Ref: https://github.com/pmndrs/jotai/issues/41
function atomWithReset<Value>(
initialValue: Value,
): WritableAtom<Value, SetStateAction<Value> | typeof RESET>Creates an atom that could be reset to its initialValue withuseResetAtom hook. It works exactly the same
way as primitive atom would, but you are also able to set it to a special valueRESET. See examples in Resettable atoms.
Example
import { atomWithReset } from 'jotai/utils'
const dollarsAtom = atomWithReset(0)
const todoListAtom = atomWithReset([
{ description: 'Add a todo', checked: false },
])RESET
Ref: https://github.com/pmndrs/jotai/issues/217
const RESET: unique symbolSpecial value that is accepted by Resettable atoms
created with atomWithReset, atomWithDefault
or writable atom created with atom if it accepts RESET symbol.
Example
import { atom, useSetAtom } from 'jotai'
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'
const dollarsAtom = atomWithReset(0)
const centsAtom = atom(
(get) => get(dollarsAtom) * 100,
(get, set, newValue: number | typeof RESET) =>
set(dollarsAtom, newValue === RESET ? newValue : newValue / 100)
)
const ResetExample = () => {
const setDollars = useSetAtom(dollarsAtom)
const resetCents = useResetAtom(centsAtom)
return (
<>
<button onClick={() => setDollars(RESET)}>Reset dollars</button>
<button onClick={resetCents}>Reset cents</button>
</>
)
}useResetAtom
function useResetAtom<Value>(
anAtom: WritableAtom<Value, typeof RESET>,
): () => void | Promise<void>Resets a Resettable atom to its initial value.
Example
import { useResetAtom } from 'jotai/utils'
import { todoListAtom } from './store'
const TodoResetButton = () => {
const resetTodoList = useResetAtom(todoListAtom)
return <button onClick={resetTodoList}>Reset</button>
}atomWithDefault
Ref: https://github.com/pmndrs/jotai/issues/352
Usage
This is a function to create a resettable primitive atom.
Its default value can be specified with a read function instead of a static initial value.
import { atomWithDefault } from 'jotai/utils'
const count1Atom = atom(1)
const count2Atom = atomWithDefault((get) => get(count1Atom) * 2)Stackblitz
Resetting default values
You can reset the value of an atomWithDefault atom to its original default value.
import { useAtom } from 'jotai'
import { atomWithDefault, useResetAtom, RESET } from 'jotai/utils'
const count1Atom = atom(1)
const count2Atom = atomWithDefault((get) => get(count1Atom) * 2)
const Counter = () => {
const [count1, setCount1] = useAtom(count1Atom)
const [count2, setCount2] = useAtom(count2Atom)
const resetCount2 = useResetAtom(count2Atom)
return (
<>
<div>
count1: {count1}, count2: {count2}
</div>
<button onClick={() => setCount1((c) => c + 1)}>increment count1</button>
<button onClick={() => setCount2((c) => c + 1)}>increment count2</button>
<button onClick={() => resetCount2()}>Reset with useResetAtom</button>
<button onClick={() => setCount2(RESET)}>Reset with RESET const</button>
</>
)
}This can be useful when an atomWithDefault atom value is overwritten
using the set function, in which case the provided getter function
is no longer used and any change in dependencies atoms will not trigger an update.
Resetting the value allows us to restore its original default value,
discarding changes made previously via the set function.
atomWithRefresh
function atomWithRefresh<Value>(
read: Read<Value, [], void>,
): WritableAtom<Value, [], void>Creates an atom that we can refresh,
which is to force reevaluating the read function.
This is helpful when you need to refresh asynchronous data.
It can also be used to implement "pull to refresh" functionality.
function atomWithRefresh<Value, Args extends unknown[], Result>(
read: Read<Value, Args, Result>,
write: Write<Value, Args, Result>,
): WritableAtom<Value, Args | [], Result | void>Passing zero arguments to set will refresh.
Passing one or more arguments to set will call "write" function.
Example
Here's how you'd use it to implement an refresh-able source of data:
import { atomWithRefresh } from 'jotai/utils'
const postsAtom = atomWithRefresh((get) =>
fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json()),
)In a component:
const PostsList = () => {
const [posts, refreshPosts] = useAtom(postsAtom)
return (
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
{/* Clicking this button will re-fetch the posts */}
<button type="button" onClick={() => refreshPosts()}>
Refresh posts
</button>
</div>
)
}Utilities Select (`docs/utilities/select.mdx`)
title: Select nav: 3.99 keywords: select published: false
selectAtom
β οΈ Unlike its name, selectAtom is provided as an escape hatch. Using it means building not 100% pure atom model. Prefer using derived atoms and use selectAtom only when equalityFn or prevSlice is unavoidable.
Signatures
function selectAtom<Value, Slice>(
anAtom: Atom<Value>,
selector: (v: Value, prevSlice?: Slice) => Slice,
equalityFn: (a: Slice, b: Slice) => boolean = Object.is,
): Atom<Slice>This function creates a derived atom whose value is a function of the original atom's value,
determined by selector.
The selector function runs whenever the original atom changes; it updates the derived atom
only if equalityFn reports that the derived value has changed.
By default, equalityFn is reference equality, but you can supply your favorite deep-equals
function to stabilize the derived value where necessary.
Examples
const defaultPerson = {
name: {
first: 'Jane',
last: 'Doe',
},
birth: {
year: 2000,
month: 'Jan',
day: 1,
time: {
hour: 1,
minute: 1,
},
},
}
// Original atom.
const personAtom = atom(defaultPerson)
// Tracks person.name. Updated when person.name object changes, even
// if neither name.first nor name.last actually change.
const nameAtom = selectAtom(personAtom, (person) => person.name)
// Tracks person.birth. Updated when year, month, day, hour, or minute changes.
// Use of deepEquals means that this atom doesn't update if birth field is
// replaced with a new object containing the same data. E.g., if person is re-read
// from a database.
const birthAtom = selectAtom(personAtom, (person) => person.birth, deepEquals)Hold stable references
As always, to prevent an infinite loop when using useAtom in render cycle, you must provide useAtom a stable reference of your atoms.
For selectAtom, we need both the base atom and the selector to be stable.
const [value] = useAtom(selectAtom(atom(0), (val) => val)) // So this will cause an infinite loopYou have multiple options in order to satisfy these constraints:
const baseAtom = atom(0) // Stable
const baseSelector = (v) => v // Stable
const Component = () => {
// Solution 1: Memoize the whole resulting atom with "useMemo"
const [value] = useAtom(useMemo(() => selectAtom(baseAtom, (v) => v), []))
// Solution 2: Memoize the inline callback with "useCallback"
const [value] = useAtom(
selectAtom(
baseAtom,
useCallback((v) => v, []),
),
)
// Solution 3: All constraints are already satisfied
const [value] = useAtom(selectAtom(baseAtom, baseSelector))
}Stackblitz
Utilities Split (`docs/utilities/split.mdx`)
title: Split nav: 3.99 keywords: select published: false
splitAtom
The splitAtom utility is designed for scenarios where you need to obtain an atom for each element in a list. It operates on read/write atoms containing a list, returning an atom that holds a list of atoms, each corresponding to an item in the original list.
Signature
A simplified type signature for splitAtom would be:
type SplitAtom = <Item, Key>(
arrayAtom: PrimitiveAtom<Array<Item>>,
keyExtractor?: (item: Item) => Key
): Atom<Array<PrimitiveAtom<Item>>>Key Features
The returned atom contains a dispatch function in the
writedirection (since v1.6.4), providing a simple way to modify the original atom with actions likeremove,insert, andmove.An optional
keyExtractorfunction can be provided as a second argument to enhance stability and performance.
Key Extractor
The splitAtom utility supports a second argument which is a key extractor function:
export function splitAtom<Item, Key>(
arrAtom: WritableAtom<Item[], [Item[]], void> | Atom<Item[]>,
keyExtractor?: (item: Item) => Key,
)Important considerations for the keyExtractor:
- If
splitAtomis used within a React render loop, thekeyExtractormust be a stable function that maintains object equality (shallow equality). This requirement doesn't apply outside of render loops. - Providing a
keyExtractorenhances the stability of the atom output (which makes memoization hit cache more often). It prevents unnecessary subatom recreation due to index shifts in the source array. keyExtractoris an optional optimization that should only be used if the extracted key is guaranteed unique for each item in the array.
Example Usage
Here's an example demonstrating the use of splitAtom:
import { Provider, atom, useAtom, PrimitiveAtom } from 'jotai'
import { splitAtom } from 'jotai/utils'
import './styles.css'
const initialState = [
{
task: 'help the town',
done: false,
},
{
task: 'feed the dragon',
done: false,
},
]
const todosAtom = atom(initialState)
const todoAtomsAtom = splitAtom(todosAtom)
type TodoType = (typeof initialState)[number]
const TodoItem = ({
todoAtom,
remove,
}: {
todoAtom: PrimitiveAtom<TodoType>
remove: () => void
}) => {
const [todo, setTodo] = useAtom(todoAtom)
return (
<div>
<input
value={todo.task}
onChange={(e) => {
setTodo((oldValue) => ({ ...oldValue, task: e.target.value }))
}}
/>
<input
type="checkbox"
checked={todo.done}
onChange={() => {
setTodo((oldValue) => ({ ...oldValue, done: !oldValue.done }))
}}
/>
<button onClick={remove}>remove</button>
</div>
)
}
const TodoList = () => {
const [todoAtoms, dispatch] = useAtom(todoAtomsAtom)
return (
<ul>
{todoAtoms.map((todoAtom) => (
<TodoItem
todoAtom={todoAtom}
remove={() => dispatch({ type: 'remove', atom: todoAtom })}
/>
))}
</ul>
)
}
const App = () => (
<Provider>
<TodoList />
</Provider>
)
export default AppThis example demonstrates how to use splitAtom to manage a list of todo items, allowing individual manipulation of each item while maintaining the overall list atom.
Utilities Ssr (`docs/utilities/ssr.mdx`)
title: SSR nav: 3.02 keywords: ssr,server,hydrate,hydration,next,nextjs,gatsby,remix,waku,framework
useHydrateAtoms
Ref: https://github.com/pmndrs/jotai/issues/340
Usage
import { atom, useAtom } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
const countAtom = atom(0)
const CounterPage = ({ countFromServer }) => {
useHydrateAtoms([[countAtom, countFromServer]])
const [count] = useAtom(countAtom)
// count would be the value of `countFromServer`, not 0.
}The primary use case for useHydrateAtoms are SSR apps like Next.js, where an initial value is e.g. fetched on the server, which can be passed to a component by props.
β οΈ Note: Although the term "hydrate" might suggest server-side usage, this hook is designed for client-side code and should be used with the 'use client' directive.
// Definition
function useHydrateAtoms(
values: Iterable<readonly [Atom<unknown>, unknown]>,
options?: { store?: Store },
): voidThe hook takes an iterable of tuples containing [atom, value] as an argument and optional options.
// Usage with an array, specifying a store
useHydrateAtoms(
[
[countAtom, 42],
[frameworkAtom, 'Next.js'],
],
{ store: myStore },
)
// Or with a map
useHydrateAtoms(new Map([[count, 42]]))Atoms can only be hydrated once per store. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.
If there is a unique need to re-hydrate a previously hydrated atom, pass the optional dangerouslyForceHydrate as true
and note that it may behave wrongly in concurrent rendering.
useHydrateAtoms(
[
[countAtom, 42],
[frameworkAtom, 'Next.js'],
],
{
dangerouslyForceHydrate: true,
},
)If there's a need to hydrate in multiple stores, use multiple useHydrateAtoms hooks to achieve that.
useHydrateAtoms([
[countAtom, 42],
[frameworkAtom, 'Next.js'],
])
useHydrateAtoms(
[
[countAtom, 17],
[frameworkAtom, 'Gatsby'],
],
{ store: myStore },
)If you are using TypeScript with target ES5, you might need as const cast on the array to preserve the tuple type.
useHydrateAtoms([
[countAtom, 42],
[frameworkAtom, 'Next.js'],
] as const)Or you may need to use a Map when passing the atom value to useHydrateAtoms. You can find a working example in the Initializing State on Render docs.
Demo
There's more examples in the Next.js section.
Utilities Storage (`docs/utilities/storage.mdx`)
title: Storage nav: 3.01 keywords: storage,localstorage,sessionstorage,asyncstorage,persist,persistence
atomWithStorage
Ref: https://github.com/pmndrs/jotai/pull/394
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
const darkModeAtom = atomWithStorage('darkMode', false)
const Page = () => {
const [darkMode, setDarkMode] = useAtom(darkModeAtom)
return (
<>
<h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1>
<button onClick={() => setDarkMode(!darkMode)}>toggle theme</button>
</>
)
}The atomWithStorage function creates an atom with a value persisted in localStorage or sessionStorage for React or AsyncStorage for React Native.
Parameters
key (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage
initialValue (required): the initial value of the atom
storage (optional): an object with the following methods:
- getItem(key, initialValue) (required): Reads an item from storage, or falls back to the
initialValue - setItem(key, value) (required): Saves an item to storage
- removeItem(key) (required): Deletes the item from storage
- subscribe(key, callback, initialValue) (optional): A method which subscribes to external storage updates.
options (optional): an object with the following properties:
- getOnInit (optional, by default false): A boolean value indicating whether to get item from storage on initialization. Note that in an SPA with
getOnIniteither not set orfalseyou will always get the initial value instead of the stored value on initialization. If the stored value is preferred setgetOnInittotrue.
If not specified, the default storage implementation uses localStorage for storage/retrieval, JSON.stringify()/JSON.parse() for serialization/deserialization, and subscribes to storage events for cross-tab synchronization.
`createJSONStorage` util
To create a custom storage implementation with JSON.stringify()/JSON.parse() for the storage option, createJSONStorage util is provided.
Usage:
const storage = createJSONStorage(
// getStringStorage
() => window.localStorage, // or window.sessionStorage, asyncStorage or alike
// options (optional)
{
reviver, // optional reviver option for JSON.parse
replacer, // optional replacer option for JSON.stringify
},
)Note: JSON.parse is not type safe. If it can't accept any types, some kind of validation would be necessary for production apps.
Server-side rendering
When using atomWithStorage in an environment that renders on the server, such as Next.js or Waku, the storage backing the atom should safely return undefined on the server.
For example, use a guarded createJSONStorage getter when using sessionStorage in a Next.js static export:
import { atomWithStorage, createJSONStorage } from 'jotai/utils'
const storage = createJSONStorage(() =>
typeof window !== 'undefined' ? window.sessionStorage : undefined,
)
const someAtom = atomWithStorage('some-key', someInitialValue, storage)Any JSX markup that depends on the value of a stored atom (e.g., a className or style prop) will use the initialValue when rendered on the server (since localStorage and sessionStorage are not available on the server).
This means that there will be a mismatch between what is originally served to the user's browser as HTML and what is expected by React during the rehydration process if the user has a storedValue that differs from the initialValue.
The suggested workaround for this issue is to only render the content dependent on the storedValue client-side by wrapping it in a custom <ClientOnly> wrapper, which only renders after rehydration. Alternative solutions are technically possible, but would require a brief "flicker" as the initialValue is swapped to the storedValue, which can result in an unpleasant user experience, so this solution is advised.
Deleting an item from storage
For the case you want to delete an item from storage,
the atom created with atomWithStorage accepts the RESET symbol on write.
See the following example for the usage:
import { useAtom } from 'jotai'
import { atomWithStorage, RESET } from 'jotai/utils'
const textAtom = atomWithStorage('text', 'hello')
const TextBox = () => {
const [text, setText] = useAtom(textAtom)
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setText(RESET)}>Reset (to 'hello')</button>
</>
)
}If needed, you can also do conditional resets based on previous value.
This can be particularly useful if you wish to clear keys in localStorage if previous values meet a condition.
Below exemplifies this usage that clears the visible key whenever the previous value is true.
import { useAtom } from 'jotai'
import { atomWithStorage, RESET } from 'jotai/utils'
const isVisibleAtom = atomWithStorage('visible', false)
const TextBox = () => {
const [isVisible, setIsVisible] = useAtom(isVisibleAtom)
return (
<>
{isVisible && <h1>Header is visible!</h1>}
<button onClick={() => setIsVisible((prev) => (prev ? RESET : true))}>
Toggle visible
</button>
</>
)
}React-Native implementation
You can use any library that implements getItem, setItem & removeItem.
Let's say you would use the standard AsyncStorage provided by the community.
import { atomWithStorage, createJSONStorage } from 'jotai/utils'
import AsyncStorage from '@react-native-async-storage/async-storage'
const storage = createJSONStorage(() => AsyncStorage)
const content = {} // anything JSON serializable
const storedAtom = atomWithStorage('stored-key', content, storage)Note set getOnInit to true if you want the atom to return the stored value immediately on initialization instead of the default value.
getOnInit true vs. false example
// Assume localStorage already contains: { "symbol": "BTC_USDC" }
// Without getOnInit (default behavior)
const symbolAtom = atomWithStorage('symbol', 'SOL_USDC')
function App() {
const symbol = useAtomValue(symbolAtom)
useEffect(() => {
console.log('symbol', symbol)
}, [symbol])
return <div>{symbol}</div>
}
// Console output WITHOUT getOnInit:
// LOG "symbol" SOL_USDC (initial render)
// LOG "symbol" BTC_USDC (after storage loads)
// With getOnInit set to true
const symbolAtom = atomWithStorage('symbol', 'SOL_USDC', undefined, {
getOnInit: true,
})
function App() {
const symbol = useAtomValue(symbolAtom)
useEffect(() => {
console.log('symbol', symbol)
}, [symbol])
return <div>{symbol}</div>
}
// Console output WITH getOnInit:
// LOG "symbol" BTC_USDC (gets stored value immediately)Notes with AsyncStorage (since v2.2.0)
With AsyncStorage (as with any asynchronous storage), the atom value becomes async.
When updating the atom by referencing the current value, then you'll need to await it.
const countAtom = atomWithStorage('count-key', 0, anyAsyncStorage)
const Component = () => {
const [count, setCount] = useAtom(countAtom)
const increment = () => {
setCount(async (promiseOrValue) => (await promiseOrValue) + 1)
}
// ...
}Validating stored values
To add runtime validation to your storage atoms, you will need to create a custom implementation of storage.
Below is an example that utilizes Zod to validate values stored in localStorage with cross-tab synchronization.
import { atomWithStorage } from 'jotai/utils'
import { z } from 'zod'
const myNumberSchema = z.number().int().nonnegative()
const storedNumberAtom = atomWithStorage('my-number', 0, {
getItem(key, initialValue) {
const storedValue = localStorage.getItem(key)
try {
return myNumberSchema.parse(JSON.parse(storedValue ?? ''))
} catch {
return initialValue
}
},
setItem(key, value) {
localStorage.setItem(key, JSON.stringify(value))
},
removeItem(key) {
localStorage.removeItem(key)
},
subscribe(key, callback, initialValue) {
if (
typeof window === 'undefined' ||
typeof window.addEventListener === 'undefined'
) {
return
}
const handler = (e) => {
if (e.storageArea === localStorage && e.key === key) {
let newValue
try {
newValue = myNumberSchema.parse(JSON.parse(e.newValue ?? ''))
} catch {
newValue = initialValue
}
callback(newValue)
}
}
window.addEventListener('storage', handler)
return () => window.removeEventListener('storage', handler)
},
})We also have a new util unstable_withStorageValidator to simplify some cases.
The above case would become:
import {
atomWithStorage,
createJSONStorage,
unstable_withStorageValidator as withStorageValidator,
} from 'jotai/utils'
import { z } from 'zod'
const myNumberSchema = z.number().int().nonnegative()
const isMyNumber = (v) => myNumberSchema.safeParse(v).success
const storedNumberAtom = atomWithStorage(
'my-number',
0,
withStorageValidator(isMyNumber)(createJSONStorage()),
)--- METRICS ---
- Files Extracted: 81
- Estimated Token Budget: ~67092 tokens
- Canonical Reference: https://codewiki.google/github.com/pmndrs/jotai