createStore
How to create vanilla stores
createStore
lets you create a vanilla store that exposes API utilities.
const someStore = createStore(stateCreatorFn)
Types
Signature
createStore<T>()(stateCreatorFn: StateCreator<T, [], []>): StoreApi<T>
Reference
createStore(stateCreatorFn)
Parameters
stateCreatorFn
: A function that takesset
function,get
function andstore
as arguments. Usually, you will return an object with the methods you want to expose.
Returns
createStore
returns a vanilla store that exposes API utilities, setState
, getState
,
getInitialState
and subscribe
.
Usage
Updating state based on previous state
This example shows how you can support updater functions within actions.
import { createStore } from 'zustand/vanilla'
type AgeStoreState = { age: number }
type AgeStoreActions = {
setAge: (
nextAge:
| AgeStoreState['age']
| ((currentAge: AgeStoreState['age']) => AgeStoreState['age']),
) => void
}
type AgeStore = AgeStoreState & AgeStoreActions
const ageStore = createStore<AgeStore>()((set) => ({
age: 42,
setAge: (nextAge) =>
set((state) => ({
age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge,
})),
}))
function increment() {
ageStore.getState().setAge((currentAge) => currentAge + 1)
}
const $yourAgeHeading = document.getElementById(
'your-age',
) as HTMLHeadingElement
const $incrementBy3Button = document.getElementById(
'increment-by-3',
) as HTMLButtonElement
const $incrementBy1Button = document.getElementById(
'increment-by-1',
) as HTMLButtonElement
$incrementBy3Button.addEventListener('click', () => {
increment()
increment()
increment()
})
$incrementBy1Button.addEventListener('click', () => {
increment()
})
const render: Parameters<typeof ageStore.subscribe>[0] = (state) => {
$yourAgeHeading.innerHTML = `Your age: ${state.age}`
}
render(ageStore.getInitialState(), ageStore.getInitialState())
ageStore.subscribe(render)
Here's the html
code
<h1 id="your-age"></h1>
<button id="increment-by-3" type="button">+3</button>
<button id="increment-by-1" type="button">+1</button>
Updating Primitives in State
State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors.
By default, set
function performs a shallow merge. If you need to completely replace
the state with a new one, use the replace
parameter set to true
import { createStore } from 'zustand/vanilla'
type XStore = number
const xStore = createStore<XStore>()(() => 0)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
xStore.setState(event.clientX, true)
})
const render: Parameters<typeof xStore.subscribe>[0] = (x) => {
$dot.style.transform = `translate(${x}px, 0)`
}
render(xStore.getInitialState(), xStore.getInitialState())
xStore.subscribe(render)
Here's the html
code
<div
id="dot-container"
style="position: relative; width: 100vw; height: 100vh;"
>
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
Updating Objects in State
Objects are mutable in JavaScript, but you should treat them as immutable when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object.
By default, set
function performs a shallow merge. For most updates where you only need to modify
specific properties, the default shallow merge is preferred as it's more efficient. To completely
replace the state with a new one, use the replace
parameter set to true
with caution, as it
discards any existing nested data within the state.
import { createStore } from 'zustand/vanilla'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
Here's the html
code
<div
id="dot-container"
style="position: relative; width: 100vw; height: 100vh;"
>
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
Updating Arrays in State
Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array.
By default, set
function performs a shallow merge. To update array values we should assign new
values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely
replace the state with a new one, use the replace
parameter set to true
.
We should prefer immutable operations like: [...array]
, concat(...)
, filter(...)
,
slice(...)
, map(...)
, toSpliced(...)
, toSorted(...)
, and toReversed(...)
, and avoid
mutable operations like array[arrayIndex] = ...
, push(...)
, unshift(...)
, pop(...)
,
shift(...)
, splice(...)
, reverse(...)
, and sort(...)
.
import { createStore } from 'zustand/vanilla'
type PositionStore = [number, number]
const positionStore = create<PositionStore>()(() => [0, 0])
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.setState([event.clientX, event.clientY], true)
})
const render: Parameters<typeof positionStore.subscribe>[0] = ([x, y]) => {
$dot.style.transform = `translate(${x}px, ${y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
Here's the html
code
<div
id="dot-container"
style="position: relative; width: 100vw; height: 100vh;"
>
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
Subscribing to state updates
By subscribing to state updates, you register a callback that fires whenever the store's state
updates. We can use subscribe
for external state management.
import { createStore } from 'zustand/vanilla'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
const $dot = document.getElementById('dot') as HTMLDivElement
$dot.addEventListener('mouseenter', (event) => {
const parent = event.currentTarget.parentElement
const parentWidth = parent.clientWidth
const parentHeight = parent.clientHeight
positionStore.getState().setPosition({
x: Math.ceil(Math.random() * parentWidth),
y: Math.ceil(Math.random() * parentHeight),
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
const logger: Parameters<typeof positionStore.subscribe>[0] = (state) => {
console.log('new position', { position: state.position })
}
positionStore.subscribe(logger)
Here's the html
code
<div
id="dot-container"
style="position: relative; width: 100vw; height: 100vh;"
>
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
Troubleshooting
I’ve updated the state, but the screen doesn’t update
In the previous example, the position
object is always created fresh from the current cursor
position. But often, you will want to include existing data as a part of the new object you’re
creating. For example, you may want to update only one field in a form, but keep the previous
values for all other fields.
These input fields don’t work because the oninput
handlers mutate the state:
import { createStore } from 'zustand/vanilla'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const personStore = createStore<PersonStore>()((set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
},
setPerson: (person) => set({ person }),
}))
const $firstNameInput = document.getElementById(
'first-name',
) as HTMLInputElement
const $lastNameInput = document.getElementById('last-name') as HTMLInputElement
const $emailInput = document.getElementById('email') as HTMLInputElement
const $result = document.getElementById('result') as HTMLDivElement
function handleFirstNameChange(event: Event) {
personStore.getState().person.firstName = (event.target as any).value
}
function handleLastNameChange(event: Event) {
personStore.getState().person.lastName = (event.target as any).value
}
function handleEmailChange(event: Event) {
personStore.getState().person.email = (event.target as any).value
}
$firstNameInput.addEventListener('input', handleFirstNameChange)
$lastNameInput.addEventListener('input', handleLastNameChange)
$emailInput.addEventListener('input', handleEmailChange)
const render: Parameters<typeof personStore.subscribe>[0] = (state) => {
$firstNameInput.value = state.person.firstName
$lastNameInput.value = state.person.lastName
$emailInput.value = state.person.email
$result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})`
}
render(personStore.getInitialState(), personStore.getInitialState())
personStore.subscribe(render)
Here's the html
code
<label style="display: block">
First name:
<input id="first-name" />
</label>
<label style="display: block">
Last name:
<input id="last-name" />
</label>
<label style="display: block">
Email:
<input id="email" />
</label>
<p id="result"></p>
For example, this line mutates the state from a past render:
personStore.getState().firstName = (e.target as any).value
The reliable way to get the behavior you’re looking for is to create a new object and pass it to
setPerson
. But here you want to also copy the existing data into it because only one of the
fields has changed:
personStore.getState().setPerson({
firstName: e.target.value, // New first name from the input
})
We don’t need to copy every property separately due to set
function performing shallow merge by
default.
Now the form works!
Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly!
import { createStore } from 'zustand/vanilla'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const personStore = createStore<PersonStore>()((set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
},
setPerson: (person) => set({ person }),
}))
const $firstNameInput = document.getElementById(
'first-name',
) as HTMLInputElement
const $lastNameInput = document.getElementById('last-name') as HTMLInputElement
const $emailInput = document.getElementById('email') as HTMLInputElement
const $result = document.getElementById('result') as HTMLDivElement
function handleFirstNameChange(event: Event) {
personStore.getState().setPerson({
...personStore.getState().person,
firstName: (event.target as any).value,
})
}
function handleLastNameChange(event: Event) {
personStore.getState().setPerson({
...personStore.getState().person,
lastName: (event.target as any).value,
})
}
function handleEmailChange(event: Event) {
personStore.getState().setPerson({
...personStore.getState().person,
email: (event.target as any).value,
})
}
$firstNameInput.addEventListener('input', handleFirstNameChange)
$lastNameInput.addEventListener('input', handleLastNameChange)
$emailInput.addEventListener('input', handleEmailChange)
const render: Parameters<typeof personStore.subscribe>[0] = (state) => {
$firstNameInput.value = state.person.firstName
$lastNameInput.value = state.person.lastName
$emailInput.value = state.person.email
$result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})`
}
render(personStore.getInitialState(), personStore.getInitialState())
personStore.subscribe(render)