pmndrs / zustand Goto Github PK
View Code? Open in Web Editor NEW🐻 Bear necessities for state management in React
Home Page: https://zustand-demo.pmnd.rs/
License: MIT License
🐻 Bear necessities for state management in React
Home Page: https://zustand-demo.pmnd.rs/
License: MIT License
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:
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.
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
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.
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!
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>
}
Please, review https://github.com/zerobias/effector and https://github.com/artalar/reatom
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;
}
Hello,
Is there a technical limitation that would explain why there are no automatic typings of the store ?
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
.
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
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;
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.
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 ] = ....
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);
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:
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.
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
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])
.
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)
}
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
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
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?
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
.
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: [...] })
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.
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.
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?
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
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?
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
Does Zustand
support react-native as well ?
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.
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)),
});
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)
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.
Many developers consider that a deciding factor for choosing one or the other store solution.
Could you give an example how to integrate that?
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)
}
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?
When updating from 1.0.7 to 2.2.1, I lose the tracking of all actions in the Redux devtools:
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
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.
Let's collect some,
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.
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
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 }));
},
}))
);
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?
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.
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
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.