Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zustand 3 milestones #71

Closed
2 tasks
drcmda opened this issue Oct 25, 2019 · 25 comments · Fixed by #148
Closed
2 tasks

zustand 3 milestones #71

drcmda opened this issue Oct 25, 2019 · 25 comments · Fixed by #148

Comments

@drcmda
Copy link
Member

drcmda commented Oct 25, 2019

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.

@JeremyRH

@JeremyRH
Copy link
Contributor

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.

@JeremyRH
Copy link
Contributor

The simpler API is definitely doable. I don't think it needs to be backwards compatible if we release it as a v3 update.

@laurib
Copy link

laurib commented Oct 30, 2019

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?

@drcmda
Copy link
Member Author

drcmda commented Oct 30, 2019

you can add them to the base object by doing

base.foo = () => ...

@drcmda drcmda pinned this issue Nov 26, 2019
@timkindberg
Copy link

timkindberg commented Dec 13, 2019

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>
  );
}

@timkindberg
Copy link

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

@timkindberg
Copy link

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 })

@timkindberg
Copy link

@drcmda @JeremyRH I know I kind of spammed up above, but any thoughts on my suggestions?

@drcmda
Copy link
Member Author

drcmda commented Jan 29, 2020

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. ;-)

@rdhox
Copy link

rdhox commented Feb 13, 2020

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 ;)

@JeremyRH
Copy link
Contributor

JeremyRH commented Feb 14, 2020

@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".

@JeremyRH
Copy link
Contributor

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.

@JeremyRH
Copy link
Contributor

Looks like a new React hook might solve this: reactjs/rfcs#147

@rdhox
Copy link

rdhox commented Feb 14, 2020

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.

@rdhox
Copy link

rdhox commented Feb 14, 2020

From what I understand from the example above, tearing happens when:

  • we have 50 components subscribed to the same (global) state, which is {count: 0}.
  • the state change to 1, React start uploading the tree.
  • when the first 10 components update, we click and the state change to 2.
  • React cancel the render of the next 40 components with the state 1, and start directly to update them with the state 2.
  • We have now the first 10 components that are showing 1, and the rest that are showing 2
  • Once the render is finished, React update the first 10 component to the actual 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.

@JeremyRH
Copy link
Contributor

Yeah that's exactly what happens and the useMutableSource hook can potentially fix it.

@timkindberg
Copy link

timkindberg commented Feb 16, 2020

@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:
#71 (comment)

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).
#71 (comment)

@JeremyRH
Copy link
Contributor

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.

@rdhox
Copy link

rdhox commented Feb 17, 2020

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

@rdhox
Copy link

rdhox commented Feb 25, 2020

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?

@rdhox
Copy link

rdhox commented Mar 10, 2020

Edit: The bug I have in the sandbox may be linked to the end of this post: reactjs/rfcs#147 (comment)

@dai-shi
Copy link
Member

dai-shi commented Aug 15, 2020

FWIW, we plan v3 for the simpler api and v4 for concurrent mode.

@timkindberg
Copy link

Some additional thoughts on API changes, might be worth considering for v3: #151

@orenmizr
Copy link

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 ?

@dai-shi
Copy link
Member

dai-shi commented Jul 29, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants