Closed drcmda closed 4 years ago
Concurrent React, are we ready for it?
Unfortunately, I don't think we are 100% ready for it. The tearing issue can be solved by creating a deep copy of the store but correctly rendering after a priority interrupt seems to require a context provider. A context provider would break the ability to use Zustand outside of React and break transient updates.
To solve this, things could be broken up into separate packages. The main package would be Zustand as we know it today (but with a context provider) for use with React. Another package would be the "core" state manager that has nothing to do with React, basically the api
object by itself. Other packages could be middlewares and/or miscellaneous functions. This would enable using the "core" state manager for transient updates and the main package for normal use with React. I think you should even be able to "link" an external store with a React store using a context provider but I'll have to think about it more.
The simpler API is definitely doable. I don't think it needs to be backwards compatible if we release it as a v3 update.
Could you please add an option to add functions or other properties to base object? E.q. useStore.dispatch() or useStore.myActions = { myFunction: () => {}};
Maybe some api function for adding stuff?
you can add them to the base object by doing
base.foo = () => ...
I think it would be cool if you could use zustand to create a singleton store from ANY hook that returns data. That way folks that have an existing custom hook managing non-singleton state, could easily upgrade it to be a singleton via zustand. Constate does this but I think zustand can do it better.
So it essentially turns the clever way of sharing state without Context into its own separate feature, apart from the opinionated state management via the set/get
paradigm. That becomes optional.
Using the Basic Example from Constate as a starter...
import React, { useState } from "react";
import createStore from "zustand";
// 1️⃣ Create a custom hook as usual
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
return { count, increment };
}
// 2️⃣ Wrap your hook
const useCounterStore = createStore(useCounter)
function Button() {
// 3️⃣ Use store instead of custom hook, make use of zustand's selector api
const increment = useCounterStore(s => s.increment);
return <button onClick={increment}>+</button>;
}
function Count() {
// 4️⃣ Use store in other components
const count = useCounterContext(s => s.count);
return <span>{count}</span>;
}
function App() {
// 5️⃣ No need for a Provider
return (
<div>
<Count />
<Button />
</div>
);
}
I was also wondering how this will play with the various data fetching libraries coming out that take advantage of Suspense such as SWR and React-Query.
Right now zustand is not able to compose other hooks at all. It's sort of a dead-end hook because the API it exposes does not follow the rules of hooks. It's a proprietary API.
I made a demo of how you might mix zustand with react-query but it feels a bit dirty :( https://codesandbox.io/s/zustand-react-query-2-u8il7
I'd like to be able to have zustand actions BE react-query hooks
Another idea for clean api would be to return the api object and have the hook be a property on it.
const store = create(set => ({ set, count: 0 }))
const count = store.use(state => state.count)
const count = store.getState().count
const unsub = store.subscribe(count => console.log(count), state => state.count)
store.setState({ count: 1 })
@drcmda @JeremyRH I know I kind of spammed up above, but any thoughts on my suggestions?
would be nice but its against hook naming convention, which means eslint would start complaining. must be useCamelCase unfortunately. personally i would prefer your version, but that's just asking for trouble going against linters like that. ;-)
Concurrent React, are we ready for it?
Unfortunately, I don't think we are 100% ready for it. The tearing issue can be solved by creating a deep copy of the store but correctly rendering after a priority interrupt seems to require a context provider. A context provider would break the ability to use Zustand outside of React and break transient updates.
To solve this, things could be broken up into separate packages. The main package would be Zustand as we know it today (but with a context provider) for use with React. Another package would be the "core" state manager that has nothing to do with React, basically the
api
object by itself. Other packages could be middlewares and/or miscellaneous functions. This would enable using the "core" state manager for transient updates and the main package for normal use with React. I think you should even be able to "link" an external store with a React store using a context provider but I'll have to think about it more.
The reason I use zustand and i'm interested into collaborate is the absence of the context api. The context is not a good solution for global state management as it re-renders automatically every subscribers, and react-redux took a step back in its V7 from it. Some other libraries try to deal with it, with success (like react-tracked), but the fact that zustand is so light and efficient and doesn't depend of the context is its force. But I am also not an expert of the constraints that are link to future concurrent mode of React. @JeremyRH, can you make a little summary of what can be the problem? thanks ;)
@rdhox You can read about and experiment with state managers and concurrent mode here. Zustand has a problem with "tearing during update", "tearing with transition", and "proper branching with transition".
The "tearing" effect is caused by components using the same state without React knowing they are using the same state.
In concurrent mode, React will do its best to update each component as fast as it can without dropping frames. It does this by interrupting renders. It seems React won't interrupt renders between components if they are using the same context. Zustand doesn't use context so React doesn't know to keep the components in sync.
Looks like a new React hook might solve this: https://github.com/reactjs/rfcs/pull/147
Thanks for your answers. I dug into it a little yesterday when I found the repo that you mentionned, and I reproduce the zustand example for convenience:
https://codesandbox.io/s/jolly-tdd-vgcko
I will continue to test and read to really understand the matter. In the sandbox, changing state with transition seems to work, only the change of state outside of react seems to be a problem.
From what I understand from the example above, tearing happens when:
{count: 0}
.1
, React start uploading the tree.2
.1
, and start directly to update them with the state 2
.1
, and the rest that are showing 2
state
.This behavior doesn't happen if the count is a local state of the parent component, or if count is a value from a context provider.
Yeah that's exactly what happens and the useMutableSource
hook can potentially fix it.
@drcmda @JeremyRH Thank you for replying to one of my comments above. Did you have any thoughts on my other two comments above?
Zustand composing other hooks e.g useSWR / React-Query: https://github.com/react-spring/zustand/issues/71#issuecomment-565255339
Zustand turning any hook into a shared hook, similar to Constate: (This one I feel would be difficult because zustand relies pretty heavily on the (set, get) architecture). https://github.com/react-spring/zustand/issues/71#issuecomment-565249121
Zustand composing other hooks e.g useSWR / React-Query: I'm sorry but I don’t understand. The example you provided doesn’t use Zustand for anything except an object store. You could have just used a plain object with the same results.
When you use React-Query's useQuery
, it creates local state for the component (data, isLoading, error
) and has control of re-rendering your component when data changes. It doesn’t make sense for Zustand to try and manage this local state because any changes Zustand makes to the data will be removed when the data is fetched again. If you only fetch the data once, you shouldn't use React-Query.
Zustand turning any hook into a shared hook, similar to Constate: (This one I feel would be difficult because zustand relies pretty heavily on the (set, get) architecture).
This would basically be the same thing as setting up a singleton but using a custom hook instead of an object with get
and set
functions. It's a neat idea but I can’t figure out how it would actually work internally. I'll have to think about it more.
For fun I tried to implement the PR hooks useMutableSource
with zustand. I have an error in place of tearing (may cause by misunderstanding the API from my part, don't really understand the subscribe argument of useMutableSource yet):
Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.
Of course we are far from a real new feature in production, but if you want to fiddle with it: https://codesandbox.io/s/vigorous-colden-6cj25
After the few changes made by bvaughn on the useMutableSource hooks, I still have some difficulties to implement it, especially with the event outside the react app which still tear the states, like in the example above. Seems to work ok with the classic update and the useTransition hooks. @JeremyRH have you got a better understanding of it?
Edit: The bug I have in the sandbox may be linked to the end of this post: https://github.com/reactjs/rfcs/pull/147#issuecomment-595354601
FWIW, we plan v3 for the simpler api and v4 for concurrent mode.
Some additional thoughts on API changes, might be worth considering for v3: https://github.com/react-spring/zustand/issues/151
Hi, how would you want developers connecting zustand and reactSWR/query today? (for some cases where you need to fetch but still want it in a global store)
is this a v4 only feature ?
zustand is unopinionated how developers connect it with others. It's the same in v4. You might want to open a new discussion for discussing best practices.
Let's collect some,
currently
why not
vanilla users wouldn't name it "useStore"
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.
@JeremyRH