A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.
Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers. It may be the one state-manager in the React space that gets all of these right.
You can try a live demo here.
npm i zustand
Your store is a hook! You can put anything in it: primitives, objects, functions. State has to be updated immutably and the set
function merges state to help it.
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
Use the hook anywhere, no providers are needed. Select your state and the component will re-render on changes.
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
- Simple and un-opinionated
- Makes hooks the primary means of consuming state
- Doesn't wrap your app in context providers
- Can inform components transiently (without causing render)
- Less boilerplate
- Renders components only on changes
- Centralized, action-based state management
You can, but bear in mind that it will cause the component to update on every state change!
const state = useBearStore()
It detects changes with strict-equality (old === new) by default, this is efficient for atomic state picks.
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)
If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can use useShallow to prevent unnecessary rerenders when the selector output does not change according to shallow equal.
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
const useBearStore = create((set) => ({
nuts: 0,
honey: 0,
treats: {},
// ...
}))
// Object pick, re-renders the component when either state.nuts or state.honey change
const { nuts, honey } = useBearStore(
useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
)
// Array pick, re-renders the component when either state.nuts or state.honey change
const [nuts, honey] = useBearStore(
useShallow((state) => [state.nuts, state.honey]),
)
// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useBearStore(useShallow((state) => Object.keys(state.treats)))
For more control over re-rendering, you may provide any custom equality function (this example requires the use of createWithEqualityFn
).
const treats = useBearStore(
(state) => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats),
)
The set
function has a second argument, false
by default. Instead of merging, it will replace the state model. Be careful not to wipe out parts you rely on, like actions.
import omit from 'lodash-es/omit'
const useFishStore = create((set) => ({
salmon: 1,
tuna: 2,
deleteEverything: () => set({}, true), // clears the entire store, actions included
deleteTuna: () => set((state) => omit(state, ['tuna']), true),
}))
Just call set
when you're ready, zustand doesn't care if your actions are async or not.
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond)
set({ fishies: await response.json() })
},
}))
set
allows fn-updates set(state => result)
, but you still have access to state outside of it through get
.
const useSoundStore = create((set, get) => ({
sound: 'grunt',
action: () => {
const sound = get().sound
...
Sometimes you need to access state in a non-reactive way or act upon the store. For these cases, the resulting hook has utility functions attached to its prototype.
const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))
// Getting non-reactive fresh state
const paw = useDogStore.getState().paw
// Listening to all changes, fires synchronously on every change
const unsub1 = useDogStore.subscribe(console.log)
// Updating state, will trigger listeners
useDogStore.setState({ paw: false })
// Unsubscribe listeners
unsub1()
// You can of course use the hook as you always would
function Component() {
const paw = useDogStore((state) => state.paw)
...
If you need to subscribe with a selector,
subscribeWithSelector
middleware will help.
With this middleware subscribe
accepts an additional signature:
subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from 'zustand/middleware'
const useDogStore = create(
subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
)
// Listening to selected changes, in this case when "paw" changes
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log)
// Subscribe also exposes the previous value
const unsub3 = useDogStore.subscribe(
(state) => state.paw,
(paw, previousPaw) => console.log(paw, previousPaw),
)
// Subscribe also supports an optional equality function
const unsub4 = useDogStore.subscribe(
(state) => [state.paw, state.fur],
console.log,
{ equalityFn: shallow },
)
// Subscribe and fire immediately
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
fireImmediately: true,
})
Zustand core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the API utilities.
import { createStore } from 'zustand/vanilla'
const store = createStore((set) => ...)
const { getState, setState, subscribe, getInitialState } = store
export default store
You can use a vanilla store with useStore
hook available since v4.
import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'
const useBoundStore = (selector) => useStore(vanillaStore, selector)
set
or get
are not applied to getState
and setState
.
The subscribe function allows components to bind to a state-portion without forcing re-render on changes. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a drastic performance impact when you are allowed to mutate the view directly.
const useScratchStore = create((set) => ({ scratches: 0, ... }))
const Component = () => {
// Fetch initial state
const scratchRef = useRef(useScratchStore.getState().scratches)
// Connect to the store on mount, disconnect on unmount, catch state-changes in a reference
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
...
Reducing nested structures is tiresome. Have you tried immer?
import { produce } from 'immer'
const useLushStore = create((set) => ({
lush: { forest: { contains: { a: 'bear' } } },
clearForest: () =>
set(
produce((state) => {
state.lush.forest.contains = null
}),
),
}))
const clearForest = useLushStore((state) => state.clearForest)
clearForest()
Alternatively, there are some other solutions.
You can persist your store's data using any kind of storage.
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useFishStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage', // name of the item in the storage (must be unique)
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
},
),
)
See the full documentation for this middleware.
Immer is available as middleware too.
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create(
immer((set) => ({
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
})),
)
const types = { increase: 'INCREASE', decrease: 'DECREASE' }
const reducer = (state, { type, by = 1 }) => {
switch (type) {
case types.increase:
return { grumpiness: state.grumpiness + by }
case types.decrease:
return { grumpiness: state.grumpiness - by }
}
}
const useGrumpyStore = create((set) => ({
grumpiness: 0,
dispatch: (args) => set((state) => reducer(state, args)),
}))
const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })
Or, just use our redux-middleware. It wires up your main-reducer, sets the initial state, and adds a dispatch function to the state itself and the vanilla API.
import { redux } from 'zustand/middleware'
const useGrumpyStore = create(redux(reducer, initialState))
Install the Redux DevTools Chrome extension to use the devtools middleware.
import { devtools } from 'zustand/middleware'
// Usage with a plain action store, it will log actions as "setState"
const usePlainStore = create(devtools((set) => ...))
// Usage with a redux store, it will log full action types
const useReduxStore = create(devtools(redux(reducer, initialState)))
One redux devtools connection for multiple stores
import { devtools } from 'zustand/middleware'
// Usage with a plain action store, it will log actions as "setState"
const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 }))
const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 }))
// Usage with a redux store, it will log full action types
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 })
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })
Assigning different connection names will separate stores in redux devtools. This also helps group different stores into separate redux devtools connections.
devtools takes the store function as its first argument, optionally you can name the store or configure serialize options with a second argument.
Name store: devtools(..., {name: "MyStore"})
, which will create a separate instance named "MyStore" in the devtools.
Serialize options: devtools(..., { serialize: { options: true } })
.
devtools will only log actions from each separated store unlike in a typical combined reducers redux store. See an approach to combining stores #163
You can log a specific action type for each set
function by passing a third parameter:
const useBearStore = create(devtools((set) => ({
...
eatFish: () => set(
(prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
undefined,
'bear/eatFish'
),
...
You can also log the action's type along with its payload:
...
addFishes: (count) => set(
(prev) => ({ fishes: prev.fishes + count }),
undefined,
{ type: 'bear/addFishes', count, }
),
...
If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an anonymousActionType
parameter:
devtools(..., { anonymousActionType: 'unknown', ... })
If you wish to disable devtools (on production for instance). You can customize this setting by providing the enabled
parameter:
devtools(..., { enabled: false, ... })
The store created with create
doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the normal store is a hook, passing it as a normal context value may violate the rules of hooks.
The recommended method available since v4 is to use the vanilla store.
import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'
const store = createStore(...) // vanilla store without hooks
const StoreContext = createContext()
const App = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)
const Component = () => {
const store = useContext(StoreContext)
const slice = useStore(store, selector)
...
Basic typescript usage doesn't require anything special except for writing create<State>()(...)
instead of create(...)
...
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type {} from '@redux-devtools/extension' // required for devtools typing
interface BearState {
bears: number
increase: (by: number) => void
}
const useBearStore = create<BearState>()(
devtools(
persist(
(set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}),
{
name: 'bear-storage',
},
),
),
)
A more complete TypeScript guide is here.
- You may wonder how to organize your code for better maintenance: Splitting the store into separate slices.
- Recommended usage for this unopinionated library: Flux inspired practice.
- Calling actions outside a React event handler in pre-React 18.
- Testing
- For more, have a look in the docs folder
Some users may want to extend Zustand's feature set which can be done using third-party libraries made by the community. For information regarding third-party libraries with Zustand, visit the doc.
zustand's People
Forkers
scrapeofdarkness trickyc0d3r dudeonyx deawx jeremyrh jensechu padamshrestha lynncubus dm385 magellol thebuilder lwittgen jfbrazeau dbismut arjunsajeev codemilli nihakue paulshen serzubkov estengrove dichuvichkin forkkit foolishwang jackzard joelmoss raizemm johnrees cryrivers cuining moemachef dashed shaunstanislauslau evatherm90 lesliesibbs mturley gotid kasaley dineshreddynaredla rdhox karlpetersson daskyrk doc22940 krnwbsn callumbooth javierh2k clawoflight huxiangtao betula-bookmarks rodrigoberlochi richardgorman inier 0xflotus pieroapretto pankerit otaciliomota ekaone millergregor olavoasantos websocket98765 venturalp joaopdmota richardlindhout lakecenter stolinski kevronosx mofaxigua anthowen tinahir gabssnake mohammedmutafa tristargod jamesthomsondev msutkowski liinkiing bugzpodder wesleycole jafin diablow akadop andrew-colman-2 hasayake97 ishhita roadmanfong jamsch dhmacs khoby-790 raknjarasoa narwold yfarhan kidqueb jaynandu lswest nejcr caesarsol itsjonq quocvi1994 jordan-gilliam sudhansu23 stfnsr hibachrachzustand's Issues
SetState & GetState Typescript declaration in create function arguments (bis)
Integration with redux devtools?
Many developers consider that a deciding factor for choosing one or the other store solution.
Could you give an example how to integrate that?
Immer + Typescript - produce state type is missing
I'm trying to use immer to simplify the process of updating nested state. However, the state
object is converted to any
within the produce function. Am I doing something wrong?
Example:
interface State {
papers: {name: string; id: string}[];
}
const stateAndActions = (set: NamedSetState<State>, get: GetState<State>) => {
return {
...initialState,
updatePaper: (id: string, name: string) => set(produce(state => {<do something here>})),
}
}
p.s. I just switched from redux to zustand and I love it!
Typescript definitions
Hi! Love the library, really fits my workflow, thank you!
I wrote some basic type-defs to help along, maybe you'll find them useful:
type ZustandCreate<T> = (set) => T;
type ZustandUse<T> = (store: T) => Partial<T>;
declare module "zustand" {
function create<T>(fn: ZustandCreate<T>): ZustandUse<T>[];
export = create;
}
api.subscribe calls every subscriber even if state doesn't change
Adding an external subscriber using api.subscribe
calls the subscriber on every setState
call even if the state does not change. @drcmda Is this expected behavior? Fixing it could cause regressions if someone relies on it.
Review another state management
Please, review https://github.com/zerobias/effector and https://github.com/artalar/reatom
Only render updated items in an array
I have an array of items in Zustand store, and if I update one, all items in the array rerender.
I have a simple example here where a store has a list of colors rendered to divs. When you click on a div it updates the items in the stored array. However, all items rerender when one is changed. Is there a way to set up Zustand so only one object rerenders when it item changes in an array?
https://codesandbox.io/s/polished-framework-59eys
import React from "react";
import ReactDOM from "react-dom";
import produce from "immer";
import create from "zustand";
export const immer = config => set => {
return Object.entries(config()).reduce(
(acc, [key, value]) => ({
...acc,
[key]:
typeof value === "function"
? (...args) => set(produce(draft => void config(draft)[key](...args)))
: value
}),
{}
);
};
const store = state => ({
divs: ["red", "red", "red"],
updateColor: (color, index) => {
state.divs[index] = color;
}
});
const [useStore] = create(immer(store));
const App = () => {
const { divs } = useStore();
return (
<div className="App">
{divs.map((d, i) => (
<DivEl color={d} index={i} />
))}
</div>
);
};
const DivEl = ({ color, index }) => {
const { updateColor } = useStore();
return (
<div
onClick={() => updateColor(color === "red" ? "blue" : "red", index)}
style={{
width: "100px",
height: "100px",
background: color,
margin: "1em"
}}
/>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
How can zustand distinguish between atomics and objects?
So this is something that @dm385 found by accidentally mutating a state object: https://codesandbox.io/s/strange-dew-4gv6k
const [useStore] = create((set, get) => ({
nested: { a: { a: 1 } }
actions: {
inc: () => {
const nested = { ...get().nested }
nested.a.a = nested.a.a + 1
set({ nested })
function Test() {
const nested = useState(state => state.nested)
Curiously the useStore hook doesn't fire when inc() is called. It mutates state, and if that weren't the case it would work, but the nested object is being exchanged here, so i was wondering why the hook didn't respond:
We distinguish between two selects, atomics and objects:
// Object pick
const { number, string } = useStore(state => ({ number: state.number, string: state.string }))
// Atomic picks
const number = useStore(state => state.number)
const string = useStore(state => state.string)
I've never thought about this before, but it actually goes into the objects we return every time to make a shallow equal test, if they're self-made, like above, or if we select them right from state:
// Doesn't do plain reference equality here, it will check each object prop for ref equality instead
const object = useStore(state => state.object)
The outcome is the same of course, if an object gets changed, even deeply, some prop is going to be different due to reducing. So at least we're safe, if users don't mutate it works, but can this lead to performance problems? For instance if a hash map object has 100.000 props it will have to go through them... : S
In Redux the hook does only a strict equality check by default, see: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates Is this something we should do? And since we can't change the call signature since the 2nd arg is already given to dependencies, would this make sense?
useStore(selector, deps)
useStore.shallow(selector, deps)
api.subscribe(selector, callback)
api.subscribe.shallow(selector, callback)
[rant] make the api better
The API feels so 2015!
I really like the API provided by react-easy-state https://github.com/solkimicreb/react-easy-state and also some similar newer libraries. They just gets out of your way and you feel like you are doing normal javascript.
Subscribers can get corrupted if state update happens during component lifecycle
The subscribers
array within the zustand store can have a subscriber overwritten and forever orphaned under the right circumstances.
This can cause a component subscribed to the store to never get updates, as its listener gets overwritten by another subscriber. Please see this minimal (yet strange) repro: https://codesandbox.io/s/quirky-taussig-ng90m
React Native Support
Does Zustand
support react-native as well ?
Devtools integration / middleware
Infinite recursion in selector
Now these awesome storage hooks as side-effect solve the task of singleton-hook, like hookleton, so that makes them useful for eg. history/routing.
Would be super-cool also to incorporate "use-effect"-purpose into the standard selector function as
let [query, setQuery] = useState({ param1, param2 })
let { value, error, loading } = useZustandStore(state => ({
value: state.fetch(query),
...state
}, [...query])
// ... in zustand-store
fetch = async (query) => {
set({loading: true, error: null, success: null})
// ..await something
set({loading: false, success, error})
return success
}
Now that selector creates nasty recusion, making that method of querying data useless.
What would be the right solution here? That is not elegant to put fetching logic in a separate useEffect(fetchingLogic, [...query])
.
Typescript automatic typings
Hello,
Is there a technical limitation that would explain why there are no automatic typings of the store ?
Doesn't work on IE-10 and IE-11
Would it be possible to support IE-10 and IE-11 by generating target files(via tsconfig) for IE-10,11 as well as modern browsers?
Currently it gives syntax error in Internet Explorer.
SSR example
I think I almost have it working using a tweaked version of the redux next.js example, but the state gets wiped out on the client
Does not trigger rerender
I intended to use zustand as singleton hook for history, as so:
import { createBrowserHistory } from 'history'
import delegate from 'delegate-it'
import create from 'zustand'
// global-ish history tracker
export const [ useHistory ] = create(set => {
let history = createBrowserHistory()
const unlisten = history.listen((location, action) => {
// NOTE: passing history object as-is does not trigger rerendering
set(history)
})
let delegation = delegate('a', 'click', e => {
e.preventDefault()
history.push(e.delegateTarget.getAttribute('href'))
});
return history
})
The problem is that passing unchanged state object to set
does not trigger rerendering, as noted in the commentary. For that purpose we have to create a clone as
set({...history})
Is that planned behavior? That isn't what expected.
Thanks
[devtools] doesn't log actions since v2
When updating from 1.0.7 to 2.2.1, I lose the tracking of all actions in the Redux devtools:
2.2.1
1.0.7
The app works just fine and the store updates correctly when using the app, it just doesn't log out the commits any more.
Using it like:
import create, { StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
export const DefaultStoreState: DefaultStateGetters = {
lastDataUpdate: new Date(),
loading: false,
showDrawer: false,
message: null,
...
}
const store: StateCreator<DefaultState> = (set, get) => ({
...DefaultStoreState,
...
})
export const [useStore, Store] = create<DefaultState>(devtools(store, 'WACHS App Store'))
Any pointers would be appreciated. Let me know if you need more detail and I will update this post.
Cheers & thanks
Patrik
subscribe type error since version 2.x
Hi,
I upgraded this morning zustand from version 1.0.7
to version 2.1.0
and I have a typescript compilation error.
First I had to change the syntax with the new subscribe
API. Before I has this:
import create from 'zustand';
interface State {
time: number;
setTime: (time: number) => void;
}
export const [useTranscriptTimeSelector, useTranscriptTimeSelectorApi] = create<
State
>(set => ({
setTime: time => set({ time }),
time: 0,
}));
useTranscriptTimeSelectorApi.subscribe(
time => (player.currentTime = time as number),
{
selector: state => state.time,
},
);
Now with the new syntax I have:
import create from 'zustand';
interface State {
time: number;
setTime: (time: number) => void;
}
export const [useTranscriptTimeSelector, useTranscriptTimeSelectorApi] = create<
State
>(set => ({
setTime: time => set({ time }),
time: 0,
}));
useTranscriptTimeSelectorApi.subscribe(
time => player.currentTime = time,
state => state.time,
);
And I have this error message:
error TS7006: Parameter 'time' implicitly has an 'any' type.
time => player.currentTime = time,
~~~~
If I refer to the readme, my syntax is good.
Thank you for your help.
zustand/shallow and zustand/middleware are not transpiled to cjs
I'm getting errors because zustand/middleware.js and zustand/shallow.js have non-supported syntax in them.
There is a .cjs.js file for each but then I have to import from 'zustand/middleware.cjs' which feels a bit weird, but it's a successful workaround.
It seems with additional non-main entries like these, they do not have the convenience of specifying main
and module
in the package.json like the main entry has. So the safest bet would probably be to have their .js
file be the lowest common demoninator, commonjs.
I'm not sure these two entries need to be exported as anything but commonjs.
I think we need the dependencies array back...
I think we need dependencies back. When I upgraded to 0.1.1 my code broke. I like to create selectors in my state and those selectors use external variable dependencies. It's probably easiest to just look at a working example of the issue.
https://codesandbox.io/s/8x2ylkzpj8
I posted this in a comment, but then realized I should probably open an issue.
Question: how to overwrite api setState ?
I'm trying to use immer in api setState, but unable to overwrite it
This doesn't seems to work:
config(fn => set(produce(fn)), get, {
...api,
setState: fn => api.setState(produce(fn)),
});
ReferenceError when using devtools middleware
Hi, I'm getting a ReferenceError: Cannot access 'state' before initialization
when using zustand(1.0.4) with the devtools middleware in a create-react-app based setup. The error is only present in development mode.
To reproduce:
npx create-react-app test-zustand && cd test-zustand
yarn
yarn add zustand
- Replace App.js with [TestApp]
yarn start
[TestApp]:
import React from "react";
import "./App.css";
import create from "zustand";
import { devtools } from "zustand/middleware";
const [useStore] = create(
devtools(set => ({
count: 1,
inc: () => set(state => ({ count: state.count + 1 })),
dec: () => set(state => ({ count: state.count - 1 }))
}))
);
function App() {
const count = useStore(state => state.count);
const inc = useStore(state => state.inc);
return (
<div className="App">
<header className="App-header">
<p>Count: {count}</p>
<button onClick={inc}>Inc</button>
</header>
</div>
);
}
export default App;
Initial state from a component?
So, I need to pass props to the creation of my store, so I can use those props to derive the initial store state. Which means I have to create the store within the component itself, so I have access to its props. But that initialises a new store each time it's called.
Is there a way to create a store from within the component without it creating a new store each time? Or perhaps a better way to set initial state of the store from a component's props?
thx
Using zustand/middleware warns about enabling devtools in tests
I'm getting superfluous warnings during tests:
● Console
console.warn node_modules/zustand/middleware.cjs.js:50
Please install/enable Redux devtools extension
console.warn node_modules/zustand/middleware.cjs.js:50
Please install/enable Redux devtools extension
console.warn node_modules/zustand/middleware.cjs.js:50
Please install/enable Redux devtools extension
console.warn node_modules/zustand/middleware.cjs.js:50
Please install/enable Redux devtools extension
console.warn node_modules/zustand/middleware.cjs.js:50
Please install/enable Redux devtools extension
Because obviously it's not finding devtools. Maybe we can either disable this warning completely (I'd be fine with that), or put it behind a if (NODE_ENV === 'development')
check or something.
[BUG] changing a state item with null fails to reload component
I'm not sure if I'm doing something wrong, but when I change state with a null value it fails to reload
Repro Steps:
- https://codesandbox.io/s/xlqk4yoqvz
- click show => shows text
- click hide => hides text
- click show
- click hide with null => text doesn't hide
Extract the logic that makes hook shareable without Provider/Context
I'm loving the look of this lib! I'm currently rewriting some of my state management as a POC using it.
I was curious, would you be able to extract the logic that makes this hook "shareable" in any component without needing a Provider? If so that extracted utility would be highly valuable to the hook community. Any hook could use this utility to convert the regular hook to a "works anywhere as a singleton" hook.
Or is the "sharing" capability too coupled with the rest of the library?
Thinking something like this:
// This hook could now be used in many places with the same reference state.
const useSharedState = share(() => {
const [state, setState] = useState(initialState)
return [state, setState]
})
Some prior art are:
- constate: https://github.com/diegohaz/constate (but still uses Provider)
- hookleton: https://github.com/bySabi/hookleton (seems to achieve what I'm talking)
Feature request: api.subscribe(callback, /*selector*/)
We need a way to bind components to the store without causing updates (for rapid micro updates that would cause lots of render requests). See: https://twitter.com/0xca0a/status/1133329007800397824
In theory this can be done using subscribe, but it can't select state. My suggestion would be an optional second arg:
const [, api] = create(set => ({
a: 1,
b: 2,
set
}))
const unsub = api.subscribe(state => console.log(state), state => state.b)
api.getState().set(state => ({ ...state, b: 3 }))
// console would say: 3
Now components could receive transient updates and also disconnect on unmount via useEffect:
function Person({ id }) {
useEffect(() =>
api.subscribe(state => /*do something*/, state => state.persons[id]),
[id]
)
@JeremyRH what's your opinion? And if you're fine with it, could you help getting this in? I'm still learning TS and i'm getting some typing issues, i had to set the internal listeners array to "StateListener-any" but it looked good so far:
const listeners: Set<StateListener<any>> = new Set()
const setState = (partialState: PartialState<State>) => {
state = Object.assign(
{},
state,
typeof partialState === 'function' ? partialState(state) : partialState
)
listeners.forEach(listener => listener(state))
}
const getState = () => state
function subscribe<U>(
callback: StateListener<U>,
selector?: StateSelector<State, U>
) {
let listener = callback
if (selector) {
let stateSlice = selector(state)
listener = () => {
const selectedSlice = selector(state)
if (!shallowEqual(stateSlice, (stateSlice = selectedSlice)))
callback(stateSlice)
}
}
listeners.add(listener)
return () => void listeners.delete(listener)
}
SetState & GetState Typescript declaration in create function arguments
The create
function typescript definition has a generic parameter that helps to define the final state that will be used : TState extends State
. This state has to extend State
(Record<string, any>
) which is relevant for a state.
Unfortunately both set
and get
parameters types don't use this generic parameter and instead are bound to State
as we can see :
createState: (set: SetState<State>, get: GetState<State>, api: any) => TState
It is not very anoying for the set
parameter, but it is really a pity that when one uses the get
method, the definition does not simply type the return to TState
instead of the very neutral State
.
Is it just a mistake ? Or is there a reason not to have used TState
in set: SetState<State>
and get: GetState<State>
? (I must admit that I would expect set: SetState<TState>
and get: GetState<TState>
instead).
I've created a (trivial) pull request : #31
useStore stops firing on state changes after component remounts
When switching between mounting two components that both calls the useStore hook, where one of them conditionally mounts a child that also calls useStore, the parent will stop rendering on state changes after remounting. Here's a minimal example on codesandbox:
https://codesandbox.io/s/nostalgic-hooks-0x63r
Repro steps are included. Let me know if you want the code pasted here instead.
Could this be related to #85 ?
edit: updated codesandbox link, messed around in the original one I posted without realizing it.
devtools broken
zustand 3 milestones
Let's collect some,
- Concurrent React, are we ready for it?
- Simpler API?
currently
const [useStore, api] = create(set => ({ set, count: 0 }))
const count = useStore(state => state.count)
const count = api.getState().count
const unsub = api.subscribe(count => console.log(count), state => state.count)
api.setState({ count: 1 })
why not
const useStore = create(set => ({ set, count: 0 }))
const count = useStore(state => state.count)
const count = useStore.getState().count
const unsub = useStore.subscribe(count => console.log(count), state => state.count)
useStore.setState({ count: 1 })
vanilla users wouldn't name it "useStore"
const api = create(set => ({ set, count: 0 }))
const count = api.getState().count
it would mean we're now api compatible with redux without doing much.
with a little bit of hacking it could also be made backwards compatible by giving it a iterable property. i've done this before with three-fibers useModel which returned an array once, but then i decided a single value is leaner.
[FR] More idiomatic solution for adding/removing state-dependent event listeners
Recently I have a use case where I need to add event listener in response to state change, using subscribe
. Currently there is no idiomatic way to remove those listeners.
Currently I'm doing it like so, but it doesn't feel right.
let listener
subscribe(
someState => {
window.removeEventListener('click', listener)
listener = () => console.log(someState)
window.addEventListener('click', listener)
},
selector
I guess an API similar to React's useEffect
with cleanup function would do the trick?
This also applies to setInterval
and setTimeout
.
Middleware in TypeScript
Subscribe<T> type error
The following type is throwing errors in TS and I think it's specifically the U | void
passed to StateListener
. TS cannot infer the shape of the state passed to the listener fn.
Seems like converting U | void
into U
fixes it but we have to explicitely pass the generic when called. I was wondering why we'd need void
in this case.
export declare type Subscribe<T extends State> = <U>(
- listener: StateListener<U | void>,
+ listener: StateListener<U>,
options?: SubscribeOptions<T, U>
) => () => void;
const [, api] = create<State>(() => ({});
api.subscribe<State>(s => {}) // This passes type checking
I have created a reproducible example: https://codesandbox.io/s/adoring-brook-264gb
Question: How would you go about detecting mutations in the store
I'm trying to debug my application to make sure there's no mutation issues.
Back in redux days I could use a middleware to detect it.
Does anyone have any insights on how to add middleware to zustand to detect mutation issues?
Persistency
Redux or unistore have redux-persist or unissist persistency solution. Is there any persistor for zustand? Mb that can be solved with simple throttled middleware?
Unable to reset/replace state
Using immer and redux middlewares.
i want to replace initial state with new state, but as zustand merges with set
, then it is quite impossible.
defaultState = {
isLoading: false,
}
replace state with redux middleware:
From immer doc:
case "loadUsers":
// OK: we return an entirely new state
return action.payload
This should replace state entirely but isLoading
is still present. How to replace entire state ?
or if i return {}
then all old values are still in place
Tried different ways. immer middleware / using immer in redux middleware for set / added immer to reducer.. But was still unable to do it
Improve Typescript support and only make the first generic type required.
As soon as you start messing with the first generic, you are quickly hit by the second and third required generics. This could probably be improved by giving those default types.
Also exporting some of the types and an interface for the store api would be a nice addition. This way we can create functions that require the store api as a parameter for example.
I've already tried implementing some of these, but sadly broke the auto-generation of the State
generic, so no auto-completion for set
and get
and so on, which probably isn't so nice for none typescript users.
Asynchronous SetState
Hi 👋
I was playing with immer and it looks like they have support for asynchronous producers.
Then I was looking at a way to have side effects in producers and update the draft accordingly to update zustand's store but set
doesn't like Promises.
So, I was wondering if having set
being able to resolve promises and update the state from the resolved value would be something interesting for zustand.
I was able to make it work but I wonder if there were any drawbacks since I have to explicitely get()
the state at the moment of producing the next state. Is it similar to how set
receives the current state?
EDIT: After a few tests I've noticed that calling setState
multiple times one after the other, doesn't quite work since they are called synchronously so the draft
being passed in to both is of the shape of what get()
returns, as soon as the async producer resolves it doesn't have the previous state update so it overrides it completely. To solve this, we'd have to use the state set
gets.
Reproduction: https://codesandbox.io/s/x3rnq036xp — You can see in the console that after the second update, the state has only 2 items instead of 3.
import produce from "immer";
import create from "zustand";
const fetchThing = () => Promise.resolve("thing");
const [, api] = create(set => {
- const setState = fn => set(produce(fn));
+ const setState = async fn => {
+ set(await produce(get(), fn));
+ };
return {
things: [],
addThing() {
+ setState(draft => {
+ draft.things.push('first thing');
+ });
+ // This will override the entire "things" slice whenever it resolves.
setState(async draft => {
draft.things.push(await fetchThing());
});
}
};
});
api.subscribe(console.log);
api.getState().addThing();
Thank you! I'm happy to contribute if people are interested in updating the behaviour of set
.
Finite state machine middleware
First of all, thanks a lot for this library. I very much enjoy its simplicity.
I'm trying to create a finite state machine middleware based on zustand
and would love to seek some feedback. Is there anything I need to look out for in particular?
Here's the implementation.
const defaultUpdater = (state, data) => ({ ...state, ...data })
const machine = (stateChart, initialData, updater = defaultUpdater) => (
set,
get,
api
) => {
api.transition = (action, data) =>
set(currentState => {
const nextState = stateChart.states[currentState.state].on[action]
return nextState
? {
...updater(currentState, data),
state: nextState
}
: currentState
})
return {
transition: api.transition,
state: stateChart.initialState,
...initialData
}
}
And this is example usage.
const IDLE = 'idle'
const ERROR = 'error'
const LOADING = 'loading'
const FETCH = 'fetch'
const FETCH_SUCCESS = 'fetch_success'
const FETCH_ERROR = 'fetch_error'
const stateChart = {
initialState: IDLE,
states: {
[IDLE]: {
on: {
[FETCH]: LOADING
}
},
[ERROR]: {
on: {
[FETCH]: LOADING
}
},
[LOADING]: {
on: {
[FETCH_SUCCESS]: IDLE,
[FETCH_ERROR]: ERROR
}
}
}
const [useStore, { transition }] = create(machine(stateChart, { todos: [] }))
transition(FETCH_SUCCESS, { todos: [...] })
Discussion: Storing hooks in the state
Is it good or bad to have hooks in your store state? It's a neat idea because you could move things like useEffect into there. My gut says "DO NOT DO THIS", but I wanted to share the thought. E.g.
export const [useCommentsStore] = create((set, get) => ({
comments: [],
async useLoadComments(commentIds) {
useEffect(() => {
const commentIds = usersStore.getState().users[userId].commentIds
set({ comments: await fetch(`/comments`, { ids: commentIds }) })
}, [commentIds])
}
}))
Then in a component:
const Component = ({ commentIds }) => {
const { comments, useLoadComments } = useCommentsStore()
useLoadComments(commentIds)
return <div>{ comments }</div>
}
However, I think it might be better to not couple the store to react via hooks usage. Just use hooks in the component instead like normal:
const Component = ({ commentIds }) => {
const { comments, loadComments } = useCommentsStore()
useEffect(() => {
loadComments(commentIds)
}, [commentIds])
return <div>{ comments }</div>
}
Some examples on how to separate actions/state in different modules
I'm considering using zustand for a large project and I'm curious about your thoughts on how separate responsibilities.
I liked how using redux I could have an actions file where each functions would be imported and composed. The issue is that the set
is not accessible then, right?
Recipe for locking initial state attribute ?
I need to lock initial state attribute so that other developers are unable to overwrite it. How could i do that?
Lets say i have state like this:
defaultState = {
isLoading: false,
data: undefined,
actions: {
getData: ()
}
}
Im using immer middleware and modified redux middleware that binds dispatch to every action on initialization .
When other devs want to reset state then they will do something like this:
case 'RESET':
return defaultState;
But now dispatch is not bind to actions anymore
Locking is probably impossible? But can someone give me an hint how to write middleware or edit redux or immer middleware to check if 'actions' belongs to modified set attributes so i could bind dispatch again ?
Edit: Can close this issue. Solution was just not to keep actions in state.
const [useStore, actions ] = ....
zombie children again
example
const [useStore, api] = create(set => ({
sketch: { type: "sketch", children: ["1"] },
"1": { type: "line", children: ["2", "3"] },
"2": { type: "point", pos: [0, 0, 0] },
"3": { type: "point", pos: [10, 0, 0] },
delete() {
console.log("------ setState ------")
set(state => ({
...state,
"1": { ...state["1"], children: ["3"] },
"2": undefined
}))
}
}))
function Point({ id }) {
console.log(" Point renders", id)
const { pos } = useStore(state => state[id])
return pos.join(" ")
}
function Line({ id }) {
console.log(" Line renders", id)
const ids = useStore(state => state[id].children)
return ids.map(id => <Point key={id} id={id} />)
}
function Sketch() {
console.log("Sketch renders")
const ids = useStore(state => state.sketch.children)
return ids.map(id => <Line key={id} id={id} />)
}
function App() {
useEffect(() => void setTimeout(() => api.getState().delete(), 2000), [])
return <Sketch />
}
live demo: https://codesandbox.io/s/crazy-frog-49vrj
what happens
Sketch
Line
Point pos=[0,0,0]
Point pos=[10,0,0]
The delete() method removed the first point and takes it out of its parents children collection. React normally renders hierarchical, so it should be fine. But for some weird reason each listener in zustand immediately goes to the components render function. It actually calls a useReducer/forceUpdate, which then triggers reacts dispatchAction > scheduleWork > requestWork > performSyncWork ... > render
const setState: SetState<TState> = partial => {
const partialState =
typeof partial === 'function' ? partial(state) : partial
if (partialState !== state) {
state = Object.assign({}, state, partialState)
//<--- the next line causes synchroneous render passes for each listener called
listeners.forEach(listener => listener())
}
}
I've tried to wrap it into batchedpdates and that seemed to fix it, now React renders in hierarchical order again, after the listeners have been triggered:
unstable_batchedUpdates(() => {
listeners.forEach(listener => listener())
})
Is there any explanation for this? And what can we do to fix it?
Discussion: Multiple stores in one component
When using multiple stores in a component, it seems I have to change the style of which I consume a store. Store conventions can cause some headaches when using multiple stores in a component—conventions like: always having an actions
key, or multiple stores may have a loaded
key.
This shows the problem:
const Component = () => {
// This is my preferred way of using zustand
const { users, loaded, actions } = useUsersStore()
// But now this code will break because of duplicate variable declarations
const { comments, loaded, actions } = useCommentsStore()
}
So my only idea is to do this:
const Component = () => {
const usersStore = useUsersStore()
const commentsStore = useCommentsStore()
// Then work with it as a domain object, which is kind of nice actually.
if (usersStore.loaded) { ... }
return <div>{ commentsStore.comments }</div>
}
However, things get a bit more complicate if I want to use selectors. Atomic selectors are promoted in the docs because they are showing simple use-cases, but those might be cumbersome here because I'd have to "namespace" each clashing variable, like so:
const Component = () => {
const usersActions = useUsersStore(state => state.actions)
const usersLoaded = useUsersStore(state => state.loaded)
const comments = useCommentsStore(state => state.comments)
const commentsActions = useCommentsStore(state => state.actions)
const commentsLoaded = useCommentsStore(state => state.loaded)
// Then work with it as a domain object, which is kind of nice actually.
if (usersLoaded) { ... }
return <div>{ comments }</div>
}
Maybe that's not so bad. Thoughts?
I'd prefer the domain objects though with a nice selection API, maybe something that uses lodash pick
and get
under the hood.
const Component = () => {
const usersStore = useUsersStore(['users', 'loaded', 'actions'])
const commentsStore = useCommentsStore(['comments', 'loaded', 'actions'])
// Then I just use the domain objects as shown before
}
The string shorthand would even be a very nice API for atomic selections...
const Component = () => {
const usersActions = useUsersStore('actions')
const usersLoaded = useUsersStore('loaded')
const comments = useCommentsStore('comments')
const commentsActions = useCommentsStore('actions')
const commentsLoaded = useCommentsStore('loaded')
}
It could support lodash get
style deep selections too...
const Component = () => {
const loadAllUsers = useUsersStore('actions.loadAll')
}
If the core lib didn't support this, would this be possible to add via a middleware?
Consider changing emoji to ⛑ (approximation of red hat).
I couldn't stop myself from starring this. Amazing project name.
I'd like to propose a feature.
⛑ is probably the closest emoji to the popular red hat so it may be a fitting choice for GitHub description emoji.
Sorry for a spam issue 😅
BTW
Isn't useState
here a typo?
https://github.com/drcmda/msga/blame/master/readme.md#L32
It selects actions from the store so it looks like useStore
used four lines above.
Enhancement: Configure `storeName` and `actionName` in `devtools` middleware
I think it would be nice to supply a store "name" to the devtools middleware to more easily distinguish which store the "setState" actions are originating from.
API might be something like this:
const [useStore] = create(devtools('ProductsStore', createFn))
Then in the Redux devtools you'd see:
@@INIT
ProductsStore setState()
As an added bonus, if there was someway to pass a name to each call to set
, for example:
const [useStore] = create(devtools('ProductsStore', (set) => ({
actions: {
loadProducts() {
set('loadProducts', state => ({ products }))
}
}
})))
Then in the Redux devtools you'd see:
@@INIT
ProductsStore > loadProducts
It would probably be good if both functions could optionally take or omit the 1st string param without throwing an error, that way there is no penalty if you don't declare a store or action name.
Persist Middleware
I'm working on a middleware that saves states into LocalStorage. I see that other people also had the same idea (#7).
I have an example working, but I'd love to learn if there is a better approach.
const isBrowser = typeof window !== "undefined"
const persistedState =
isBrowser
? JSON.parse(localStorage.getItem("sidebarState"));
: null
const persist = config => (set, get, api) =>
config(
args => {
set(args);
isBrowser && localStorage.setItem("sidebarState", JSON.stringify(get()));
},
get,
api
);
const [useSidebar] = create(
persist(set => ({
isOpen: persistedState ? persistedState.isOpen : true,
toggleSidebar: () => {
set(state => ({ ...state, isOpen: !state.isOpen }));
},
}))
);
Discussion: Accessing store from another store
Is it good or bad to have stores talk to each other? If it's ok, what would be good/bad ways of going about it? E.g. Here's one idea, if stores do use other stores, they could use the exposed API instead of the hook. I'm making this use-case up, so it might be unrealistic.
// UsersStore.js
export const [useUsersStore, usersStore] = create(...)
// CommentsStore.js
import { usersStore } from './UsersStore.js'
export const [useCommentsStore, commentsStore] = create(set => ({
async loadUserComments(userId) {
// Note: I'm accessing the other store here
const commentIds = usersStore.getState().users[userId].commentIds
set({ comments: await fetch(`/comments`, { ids: commentIds }) })
}
}))
Or would it be better to force components to compose stores together like this?
// UsersStore.js
export const [useUsersStore] = create(...)
// CommentsStore.js
export const [useCommentsStore] = create(set => ({
comments: [],
async loadComments(commentIds) {
set({ comments: await fetch(`/comments`, { ids: commentIds }) })
}
}))
// Component.jsx
function UserComments({ id }) {
// Note: I'm doing all the wiring together in the component instead
const commentIds = useUsersStore(state => state.users[id].commentIds)
const comments = useCommentsStore(state => state.comments)
const loadComments = useCommentsStore(state => state.loadComments)
useEffect(() => {
loadComments(commentIds)
}, [commentIds])
}
Maybe, if you really want to encapsulate the logic, you could create a custom hook to wire them together? We could export an additional non-zustand custom hook that joins two (or more) zustand stores together to provide composited functionality.
Would this even work? I think I like this...
// Not sure what file I'd put this in ¯\_(ツ)_/¯
export const useUserComments = (userId) => {
const commentIds = useUsersStore(state => state.users[id].commentIds)
const comments = useCommentsStore(state => state.comments)
const loadComments = useCommentsStore(state => state.loadComments)
useEffect(() => {
loadComments(commentIds)
}, [commentIds])
return comments
}
// Component.jsx
function UserComments({ id }) {
const comments = useUserComments(id)
}
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.