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

Proposal: useStoreProxy hook #263

Closed
wants to merge 2 commits into from
Closed

Conversation

hmans
Copy link

@hmans hmans commented Dec 15, 2020

Summary

This is a proposal for a new hook named useStoreProxy. This hook wraps around a store created with zustand's create function and provides what I feel is more convenient access to the store data while maintaining reactive properties. It's inspired by both valtio and me getting tired of writing selector functions:

const FooCounter = () => {
  const snapshot = useStoreProxy(store)
  return <p>Foo: {snapshot.foo}</p>
}

Or:

const FooCounter = () => {
  const { foo } = useStoreProxy(store)
  return <p>Foo: {foo}</p>
}

Both of these will only rerender if any property of the snapshot is accessed, and only if that specific property is changed: essentially, useStoryProxy(store).foo is 100% equivalent to store(s => s.foo).

Accessing multiple values reactively becomes very straight-forward:

const ClickyGame = () => {
  const snapshot = useStoreProxy(store)

  return (
    <>
      <p>Count: {snapshot.count}</p>
      <button onClick={snapshot.api.increaseCounter} />
    </>
  )
}

The implementation makes use of a Proxy object, so the usual caveats apply. It should be noted that I've added code to the Proxy that specifically forbids setting instead of reading state.

Example Sandbox:

https://codesandbox.io/s/broken-https-bgq29?file=/src/App.tsx

Caveats:

  • It uses a Proxy, so it will only work in environments that support it.
  • For non-shallow stores, accessing deeper properties will not be as efficient as doing it through a selector function. For example, in the ClickyGame example above, snapshot.api.increaseCounter will make the component re-render every time snapshot.api changes, not snapshot.api.increaseCounter. Theoretically, there could be some magic here to wrap nested objects in similar proxies, but this would possible be non-trivial, and may move Zustand too close to Valtio.

Checklist:

  • Implementation
  • Tests
  • Feedback? :)
  • Documentation
  • Naming (maybe there's something better than useStoreProxy?)

Complementary Kitten Picture:

image

@codesandbox-ci
Copy link

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 32efa68:

Sandbox Source
React Configuration
React Typescript Configuration

@Tirzono
Copy link
Contributor

Tirzono commented Dec 16, 2020

I like it and I would probably use it.

Theoretically, there could be some magic here to wrap nested objects in similar proxies, but this would possible be non-trivial, and may move Zustand too close to Valtio.

I would also be interested if this would be implemented. I wouldn't mind it bringing Zustand closer to Valtio, because why not using good features of other packages?

@hmans
Copy link
Author

hmans commented Dec 16, 2020

I would also be interested if this would be implemented. I wouldn't mind it bringing Zustand closer to Valtio, because why not using good features of other packages?

Wrapping an entire tree of objects in proxies does not come for free or without problems, and I'm glad Zustand doesn't do it (and it's also the reason why I still prefer it over Valtio in my project, since I'm putting references to complex objects into my stores and 1. don't need them to be proxied, and 2. the proxying of them causes issues.)

Stores that work with explicit mutations and selectors are a different approach to stores that are transparently mutable and subscribable through proxy magic, and Zustand and Valtio respectively each serve these approaches fine. The intention of the proposal above is not to turn Zustand into a proxy-based solution, but rather just provide a more convenient way to perform the most basic way to access the store.

@hmans
Copy link
Author

hmans commented Dec 17, 2020

Exploring this further, I sort of accidentally ended up building my own state management library, which I am both proud and not proud of. :)~ If you want to take a look, here's Statery, but please consider it mostly experimental at this stage -- there's probably a whole bunch of use cases where it'll break that I haven't encountered yet. (I am using it in a real project, though, where it has been working just fine for what I'm doing with it.)

With this, I'm closing this PR for the time being. Thanks for the feedback (also on Discord), everybody!

@hmans hmans closed this Dec 17, 2020
@dai-shi
Copy link
Member

dai-shi commented Dec 17, 2020

This proposal was actually helpful to me personally.
@hmans 's work and @Tirzono 's comment made me realize that there's a demand for render optimization for zustand users.
I've been working on react-tracked for quite a while, which was targeting React Context use case. Just noticed this can turn into something for zustand. And, it worked!
dai-shi/react-tracked#71
Actually, since the implementation in v1.5.0 was already based on use-context-selector, it was just tweaking some code to export a new API createTrackedSelector.
The use case in mind is not only zustand but also react-redux.

Here's the codesandbox with minimal example: https://codesandbox.io/s/react-typescript-forked-myfki?file=/src/App.tsx

Note: react-tracked and valtio use same internal library under the hood. So, the behavior and performance should be basically equivalent.

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 this pull request may close these issues.

3 participants