Git Product home page Git Product logo

zustand's Introduction

Build Status Build Size Version Downloads Discord Shield

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 install zustand # or yarn add zustand or pnpm add zustand

โš ๏ธ This readme is written for JavaScript users. If you are a TypeScript user, be sure to check out our TypeScript Usage section.

First create a store

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

Then bind your components, and that's it!

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

Why zustand over redux?

Why zustand over context?

  • Less boilerplate
  • Renders components only on changes
  • Centralized, action-based state management

Recipes

Fetching everything

You can, but bear in mind that it will cause the component to update on every state change!

const state = useBearStore()

Selecting multiple state slices

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) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// 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.

const treats = useBearStore(
  (state) => state.treats,
  (oldTreats, newTreats) => compare(oldTreats, newTreats),
)

Overwriting state

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

Async actions

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

Read from state in actions

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

Reading/writing state and reacting to changes outside of components

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.

โš ๏ธ This technique is not recommended for adding state in React Server Components (typically in Next.js 13 and above). It can lead to unexpected bugs and privacy issues for your users. For more details, see #2200.

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

Using subscribe with selector

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

Using zustand without React

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)

โš ๏ธ Note that middlewares that modify set or get are not applied to getState and setState.

Transient updates (for often occurring state-changes)

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)
  ), [])
  ...

Sick of reducers and changing nested states? Use Immer!

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.

Persist middleware

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

Can't live without redux-like reducers and action types?

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

Redux devtools

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

Logging Actions

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 }),
    false,
    'bear/eatFish'
  ),
  ...

You can also log the action's type along with its payload:

  ...
  addFishes: (count) => set(
    (prev) => ({ fishes: prev.fishes + count }),
    false,
    { 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, ... })

React context

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

TypeScript Usage

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.

Best practices

Third-Party Libraries

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.

Comparison with other libraries

zustand's People

Contributors

anatolelucet avatar aslemammad avatar barbogast avatar barelyhuman avatar carnageous avatar charkour avatar chrisk-7777 avatar dai-shi avatar dbritto-dev avatar dependabot[bot] avatar devanshj avatar drcmda avatar exalted100 avatar gsimone avatar hchlq avatar holgergp avatar jeremyrh avatar lucasrabiec avatar lynncubus avatar mayank1513 avatar paulshen avatar qcza avatar rec0il99 avatar rnike avatar sewera avatar tbor00 avatar tkdodo avatar wadamek65 avatar wmcb91 avatar xlboy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

zustand's Issues

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.

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?

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 ] = ....

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

[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.

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!

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.

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

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?

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.

@JeremyRH

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.

[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

image

1.0.7

image

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

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

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

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.

devtools broken

Using 2.2.0 and the devtools middleware is broken due to an incorrect guard going from this to this

You now have this...

!initialState.dispatch && ignoreState && 'setState'

when it should be this...

!initialState.dispatch && !ignoreState && 'setState'

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:

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

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?

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:

  1. npx create-react-app test-zustand && cd test-zustand
  2. yarn
  3. yarn add zustand
  4. Replace App.js with [TestApp]
  5. 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;

Screenshot:
Screenshot from 2019-08-20 09-44-37

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.

Lynncubus@b095e8c

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?

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: [...] })

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

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

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.

https://github.com/react-spring/zustand/blob/f936e3d9df0e3a18e2504e62828b23f6e9937fe3/src/index.ts#L23-L25

image

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

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.

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

Middleware in TypeScript

Hi, I'd like to use zustand with TypeScript. Any chance you can write the example middlewares (log and immer) with TypeScript so I can follow and imitate?

I'd really appreciate it. Thank you. @JeremyRH @drcmda.

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

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

zombie children again

Screenshot 2019-10-03 at 16 19 32

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?

@JeremyRH

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)

@JeremyRH

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.

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.