Skip to content

Latest commit

 

History

History
300 lines (226 loc) · 10.9 KB

core.mdx

File metadata and controls

300 lines (226 loc) · 10.9 KB
title description nav
Core
This doc describes core `jotai` bundle.
2.01

Basic APIs

atom

Use atom to create an atom config. An atom config is an immutable object. The atom config doesn't hold an atom value. The atom value is stored in a Provider state.

// primitive atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>

// read-only atom
function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>

// writable derived atom
function atom<Value, Update>(
  read: (get: Getter) => Value | Promise<Value>,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>

// write-only derived atom
function atom<Value, Update>(
  read: Value,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>
  • initialValue: the initial value that the atom will return until its value is changed.
  • read: a function that's called on every re-render. The signature of read is (get) => Value | Promise<Value>, and get is a function that takes an atom config and returns its value stored in Provider as described below. Dependency is tracked, so if get is used for an atom at least once, the read will 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 of useAtom, the useAtom()[1]. The default value of this function in the primitive atom will change the value of that atom. The signature of write is (get, set, update) => void | Promise<void>. get is similar to the one described above, but it doesn't track the dependency. set is a function that takes an atom config and a new value which then updates the atom value in Provider. update is an arbitrary value that we receive from the updating function returned by useAtom described below.
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

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

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 first used in a provider, and onUnmount is called when it’s no longer used. In some edge cases, 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
}

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' })
}

useAtom

// primitive or writable derived atom
function useAtom<Value, Update>(
  atom: WritableAtom<Value, Update>,
  scope?: Scope
): [Value, SetAtom<Update>]

// read-only atom
function useAtom<Value>(atom: Atom<Value>, scope?: Scope): [Value, never]

The useAtom hook is to read an atom value stored in the Provider. It returns the atom value and an updating function as a tuple, just like useState. It takes an atom config created with atom(). Initially, there is no value stored in the Provider. The first time the atom is used via useAtom, it will add an initial value in the Provider. If the atom is a derived atom, the read function is executed to compute an 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 is removed from the Provider.

const [value, updateValue] = useAtom(anAtom)

The updateValue takes one argument, which will be passed to the third argument of writeFunction of the atom. The behavior depends on how the writeFunction is implemented.


Notes

How atom dependency works

To begin with, let's explain this. In the current implementation, every time we invoke the "read" function, we refresh the dependencies and dependents. For example, If A depends on B, it means that B is a dependency of A, and A is a dependent of B.

const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())

The read function is the first parameter of the atom. The dependency will initially be empty. On first use, we run the read function and know that uppercaseAtom depends on textAtom. textAtom has a dependency on uppercaseAtom. So, add uppercaseAtom to the dependents of textAtom. When we re-run the read function (because its dependency textAtom is updated), the dependency is created again, which is the same in this case. We then remove stale dependents and replace with the latest one.

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 cache atoms somewhere globally. See this example or that example.

Check atomFamily in utils for parameterized atoms.

Additional APIs

Provider

const Provider: React.FC<{
  initialValues?: Iterable<readonly [AnyAtom, unknown]>
  scope?: Scope
}>

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 also has some capabilities described below, which doesn't exist in the provider-less mode.

const Root = () => (
  <Provider>
    <App />
  </Provider>
)

initialValues prop

A Provider accepts an optional prop initialValues, with which you can specify some initial atom values. The use cases of this are testing and server side rendering.

Example

const TestRoot = () => (
  <Provider
    initialValues={[
      [atom1, 1],
      [atom2, 'b'],
    ]}>
    <Component />
  </Provider>
)

TypeScript

The initialValues prop is not type friendly. We can mitigate it by using a helper function.

const createInitialValues = () => {
  const initialValues: (readonly [Atom<unknown>, unknown])[] = []
  const get = () => initialValues
  const set = <Value>(anAtom: Atom<Value>, value: Value) => {
    initialValues.push([anAtom, value])
  }
  return { get, set }
}

scope prop

A Provider accepts an optional prop scope that you can use for a scoped Provider. When using atoms with a scope, the provider with the same scope is used. The recommendation for the scope value is a unique symbol. The primary use case of scope is for library usage.

Example

const myScope = Symbol()

const anAtom = atom('')

const LibraryComponent = () => {
  const [value, setValue] = useAtom(anAtom, myScope)
  // ...
}

const LibraryRoot = ({ children }) => (
  <Provider scope={myScope}>{children}</Provider>
)

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 create a write only atom where the value never changes you can use the useSetAtom hook. useSetAtom is premature optimization in this scenario.

For primitive values if you use const [, setValue] = useAtom(valueAtom) it can cause unnecessary re-renders, so useSetAtom helps to avoid those extra re-renders.

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.

Some more notes about atoms

  • If you create a primitive atom, it will use predefined read/write functions to emulate useState behavior.
  • If you create an atom with read/write functions, they can provide any behavior with some restrictions as follows.
  • read function will be invoked during React render phase, so the function has to be pure. What is pure in React is described here.
  • write function will be invoked where you called initially and in useEffect for following invocations. So, you shouldn't call write in render.
  • When an atom is initially used with useAtom, it will invoke read function to get the initial value, this is recursive process. If an atom value exists in Provider, it will be used instead of invoking read function.
  • Once an atom is used (and stored in Provider), it's value is only updated if its dependencies are updated (including updating directly with useAtom).