skip to content

In Jotai, what changes when you wrap part of the tree in a Provider, and how do createStore and getDefaultStore relate to it?

level: middleimportance: should knowfreq 30%

answer

  1. values live in stores, not atoms
  2. what happens with no Provider
  3. one store per Provider mount
  4. nearest one wins
  5. get, set and sub

basics

~20 s

A Provider gives its subtree its own store, so the same atoms hold separate values there; without one, hooks use the default store from getDefaultStore. createStore makes a store to pass to Provider and to use outside React.

solid answer

~40 s

Jotai atoms hold no values; a **store** does. With no `Provider`, every hook uses the **default store**, the one `getDefaultStore()` returns, so the app has a single global set of values. Wrapping a subtree in `<Provider>` creates a new store once per mount of that Provider, and hooks inside it read and write that store instead; the nearest Provider wins. That is how two copies of the same sign-up form, built from the same module-level atoms, keep independent values, and how a server render gets a fresh store per request. `createStore()` makes a store explicitly: pass it as `<Provider store={myStore}>` to share it with non-React code, which uses `store.get(atom)`, `store.set(atom, value)` and `store.sub(atom, listener)`. Remounting a Provider, for example by changing its `key`, discards its store and resets every atom inside.

code

tsx · 32 lines
tsx
import { useState } from 'react'
import { Provider, createStore } from 'jotai'
import { emailAtom } from './form-atoms'
import { SignupForm } from './signup-form'

// two independent copies of the same form: one store each
export function Page() {
  return (
    <>
      <Provider>
        <SignupForm />
      </Provider>
      <Provider>
        <SignupForm />
      </Provider>
    </>
  )
}

// a store shared with non-React code
export function DraftSavingForm() {
  const [store] = useState(() => {
    const s = createStore()
    s.sub(emailAtom, () => localStorage.setItem('draft-email', s.get(emailAtom)))
    return s
  })
  return (
    <Provider store={store}>
      <SignupForm />
    </Provider>
  )
}

go deeper

for a junior

Know that Jotai works without a Provider by using a default store, and that a Provider gives its subtree its own set of atom values.

for a middle

Explain the lookup order for a store, what createStore returns, and how get, set and sub let code outside React share the same values.

for a senior

Use Providers deliberately: per-request stores for server rendering, one-time hydration with useHydrateAtoms, keyed remounts to reset, and fresh stores per test.

for a principal

Decide which state is truly global and which belongs to a scoped store per feature or instance, and make that boundary visible in the component tree.

## Atoms describe state; stores hold it A Jotai atom is a config object. The values are kept in a **store**, a structure that maps each atom to its current value, its dependencies and its subscribers. Hooks such as `useAtom`, `useAtomValue` and `useSetAtom` find the store to use through Jotai's own `useStore()`, which picks, in order: 1. a store passed explicitly in the hook's options (`useAtomValue(a, { store })`); 2. the store of the **nearest `Provider`** above the component; 3. the **default store**, returned by `getDefaultStore()`. ## Provider-less mode Most small apps never render a `Provider`. Every hook then falls through to the default store, created lazily on first use. That gives one global value per atom, which is exactly what you want in a single-page app with one form, and it is what code outside React reaches with `getDefaultStore().get(emailAtom)`. ## What a Provider changes `<Provider>` without props creates a new store the first time it renders and keeps it for as long as that Provider stays mounted. Inside it: - the **same atom configs** resolve to **separate values**, starting from their initial values; - writes stay in that store and never reach the default store or a sibling Provider's store; - nested Providers shadow outer ones, so each component uses the closest store. | Setup | Which store the form's hooks use | Values shared with | |---|---|---| | No Provider | the default store | every other Provider-less reader | | `<Provider>` around each form | one new store per Provider mount | only components inside that Provider | | `<Provider store={formStore}>` | the store you created | any code holding `formStore` | The form scenario makes this concrete: a page shows an inline sign-up form and the same form in a modal. Both use the module-level `emailAtom`, `passwordAtom` and `isValidAtom`. Wrapping each in its own `Provider` gives each a separate email and a separate `isValid`, with no change to the atoms. ## `createStore` and using a store outside React `createStore()` returns a new, empty store with three methods: - `store.get(atom)` reads a value, computing derived atoms as needed; - `store.set(atom, ...args)` writes a writable atom, exactly as a hook's setter would; - `store.sub(atom, listener)` subscribes and returns an unsubscribe function. Pass that store to `<Provider store={formStore}>` and the React subtree and your plain modules — an analytics hook, a draft-saving timer, a test — share one set of values. ## Server rendering and hydration On a server that renders many requests, the default store would be shared by all of them. The Jotai docs recommend a `Provider` at the root: the app tree is created per request, so the Provider's store lives for one request. To seed atoms with data the server already has, `useHydrateAtoms` from `jotai/utils` writes the given values into the current store the first time it runs for that store, and ignores later calls for the same atoms unless `dangerouslyForceHydrate` is set. ## Resetting and testing - **Reset a whole form:** give its `Provider` a `key` and change it after a successful submit; the Provider remounts, its store is discarded, and every atom starts again from its initial value. - **Isolate tests:** render each test's component inside a fresh `Provider`, or pass a `createStore()` store and assert on it with `store.get`. - **Suspense placement:** when async atoms are involved, the docs ask for at least one `<Suspense>` inside the `Provider`. ## Provider or explicit store: a quick guide - Use **no Provider** for a single client-side app with one instance of each feature. - Use **`<Provider>` without props** when a subtree needs private values and nothing outside React touches them. - Use **`<Provider store={createStore()}>`**, created once, when plain code must read, write or subscribe to the same values the subtree sees. - Use **the `store` hook option** only for the rare component that must reach a store other than its nearest Provider's. ## Common mistakes - Expecting a value written with `getDefaultStore().set(...)` to appear inside a `Provider` subtree; that subtree reads its own store. - Creating a store in a component body with `createStore()` on every render, which discards state each time; create it once, for example with a lazy `useState`. - Adding a `Provider` "to make Jotai work"; it is optional, and adding one changes which values components see.

  • A Jotai app calls getDefaultStore().set(emailAtom, 'x') but a form inside a Provider still shows an empty email. Why?
    The form's hooks read the store of their nearest `Provider`, not the default store. The write went to the default store, which that subtree never consults. Write to the Provider's store instead: create it with `createStore()`, pass it as `<Provider store={formStore}>`, and call `formStore.set(emailAtom, 'x')`.
  • How does useHydrateAtoms behave if the component calling it re-renders with new server data?
    It hydrates each atom only once per store: it records hydrated atoms in a per-store set and skips them on later calls, so new values are ignored. That prevents it from overwriting user edits on re-render. Passing `dangerouslyForceHydrate: true` in its options writes the values again.

saying these in an interview costs you the question

  • Jotai needs a Provider at the root or atoms will not work.
  • An atom written in the default store is visible inside every Provider.
  • Each Provider creates a new store on every render of its parent.
  • useHydrateAtoms overwrites atom values every time the component renders.
  • To give two form instances separate values you must duplicate every atom.