Skip to main content

Atom State

The quick start showed how to use the useAtomState hook to get and update an atom instance's state from a React component. Calling the setState function returned from useAtomState triggers an "external update". But atoms can also update themselves.

you will learn:
  • How to use stores to create local atom state
  • How to use stores to give an atom control of its own state
  • How to attach simple side effects to atoms

Local State

In React, any component can declare local state using useState or useReducer:

function MyComponent() {
const [state, setState] = useState('initial state')
...
}

This state can affect everything else in the component - what effects to run, what happens inside callbacks, and even what UI to return. Wouldn't it be amazing if atoms had this same capability?

Well they do! In Zedux you can give an atom its own internal, local state using an injector:

injectStore

Stores are the secret sauce of Zedux. And luckily, they're extremely simple to create:

import { atom, injectStore } from '@zedux/react'

const exampleAtom = atom('example', () => {
const store = injectStore('initial state')
})

This creates a "zero-config" store with 'initial state' as its ... initial state. Whenever this store's state updates, Zedux will reevaluate this atom, calling this state factory again.

You can use the current state of the store exactly like local state in React - to run side effects, change callbacks, and even change the atom's state.

Live Sandbox
12345678910111213141516171819202122
const multiplyAtom = atom('multiply', (num: number) => {
// we'll look at updating local store state in a second
const store = injectStore(2)

return num * store.getState()
})

function Multiply() {
const [num, setNum] = useState(1)
const [multiplied] = useAtomState(multiplyAtom, [num])

return (
<>
<div>Multiplied: {multiplied}</div>
<input
onChange={event => setNum(Number(event.target.value))}
type="number"
value={num}
/>
</>
)
}

injectStore returns the same store reference every time the state factory runs. We'll see shortly why this is important.

note

injectStore is the injector equivalent of both useState and useReducer. Zedux doesn't have an injectState or injectReducer. This is because Zedux stores can be either zero-config or reducer-driven, so they cover both use cases. You'll probably use injectStore a lot!

createStore

You can create stores completely outside atoms using createStore():

import { createStore } from '@zedux/react'

const store = createStore(null, 'initial state')

Notice that the initial state is the 2nd param of createStore. Don't worry about that first param yet; just pass null for zero-config stores.

injectStore has a function overload that allows you to create the store manually:

import { atom, createStore, injectStore } from '@zedux/react'

const customStoreAtom = atom('customStore', () => {
const store = injectStore(() => createStore(null, 'initial state'))
...
})

We'll look at this more later.

Stores

Stores are so cool, they get their own walkthrough. For now, we're gonna stick to zero-config stores. Here's a brief rundown:

const store = injectStore({ text: 'initial state' })

// get the state with `getState`:
store.getState() // { text: 'initial state' }

// overwrite state with `setState`:
store.setState({ newText: 'new state' })
store.getState() // { newText: 'new state' }

// recursively merge new state into existing state with `setStateDeep`:
store.setStateDeep({ newerText: 'even newer state' })
store.getState() // { newText: 'new state', newerText: 'even newer state' }

// both setState and setStateDeep have function overloads:
store.setState(currentState => currentState.newText)
store.getState() // 'new state'

To see this in action, let's get some side effects going:

injectEffect

A simple way to run side effects in React components is with useEffect. Atoms have an injector equivalent called injectEffect that works exactly the same!

Let's use this to create a side effect that updates a local store:

Live Sandbox
12345678910111213141516171819
const secondsAtom = atom('seconds', () => {
const store = injectStore(0)

injectEffect(() => {
const intervalId = setInterval(() => {
store.setState(state => state + 1)
}, 1000)

return () => clearInterval(intervalId) // remember cleanup
}, [])

return store.getState() // <- don't do this! (see next section)
})

function Seconds() {
const [seconds] = useAtomState(secondsAtom)

return <div>seconds: {seconds}</div>
}
remember!

injectStore and injectEffect are injectors. Don't use them in loops or if statements or after any early returns.

You have now learned how to use local state inside atoms. But the above example has a huge problem: Zedux will still automatically create a store for this atom, even though we made our own. This means consumers of the atom will interact with a different store than the one we use internally. Yikes!

What we really want is to:

Expose the Store

Remember that every atom instance has its own store? Zedux creates this store automatically by default. But now that you know how to create your own stores, you can tell Zedux that it doesn't need to create one for you.

How do you do that?

Return The Store!

Live Sandbox
1234567891011
const greetingAtom = atom('greeting', () => {
const store = injectStore('Hello, World!')

return store // <- the magic!
})

function Greeting() {
const [state] = useAtomState(greetingAtom)

return <div>{state}</div>
}

By returning the store from the state factory, you essentially tell Zedux, "Don't create a store for me! I created my own. Use this one."

important

When returning a store from a state factory, ensure that the exact same store reference is returned every time the atom evaluates - e.g. by using injectStore (recommended) or injectMemo.

Joint Updates

With the store exposed, you can now update it both internally and externally. Let's expose the store in the counter example from earlier to see this in action:

Live Sandbox
123456789101112131415161718192021222324
const secondsAtom = atom('seconds', () => {
const store = injectStore(0)

injectEffect(() => {
const intervalId = setInterval(() => {
store.setState(state => state + 1)
}, 1000)

return () => clearInterval(intervalId) // remember cleanup
}, [])

return store // just return the store!
})

function Seconds() {
const [seconds, setSeconds] = useAtomState(secondsAtom)

return (
<>
<div>seconds: {seconds}</div>
<button onClick={() => setSeconds(state => state + 1)}>Increment</button>
</>
)
}

Try clicking the button to make the counter count faster. The secondsAtom now controls its own state and simultaneously allows dependents to update it.

Multiple Stores

An atom state factory can inject any number of stores. But only one store can be returned. Having unexposed "internal" stores is completely fine. But what if you want to expose multiple stores?

Zedux stores have an Ultra Mega Superpower: They are composable.

Live Sandbox
1234567891011121314151617181920212223242526272829303132
const multipleStoresAtom = atom('multipleStores', () => {
const a = injectStore(1)
const b = injectStore(10)
const store = injectStore()

// .use() configures the store after creation:
store.use({ a, b }) // <- the magic!

return store
})

function MultipleStores() {
const [state, setState] = useAtomState(multipleStoresAtom)

const incrementA = () => {
setState(state => ({ ...state, a: state.a + 1 }))
}

const incrementB = () => {
setState(state => ({ ...state, b: state.b + 1 }))
}

return (
<>
<div>
a: {state.a}, b: {state.b}
</div>
<button onClick={incrementA}>Increment A</button>
<button onClick={incrementB}>Increment B</button>
</>
)
}

To return multiple stores, create a single "parent" store composed of all the stores you want to expose and return that. Calling store.use() like this is the recommended way to create a composed store in an atom.

tip

You've just seen your first configured (not zero-config) store! We'll learn a lot more about this in the stores walkthrough.

Recap

  • Use injectStore to create a stable store reference for holding local state.
  • An injected store can be returned from a state factory to expose it as the atom instance's store.
  • injectEffect is an easy way to attach side effects to atoms.
  • Use store composition to expose multiple stores.

Next Steps

Now that you know a bit more about creating atoms, it's time to learn what atom instances look like.