Git Product home page Git Product logo

zustand's Issues

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.

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:

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

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.

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!

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

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

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.

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

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'

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

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

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.

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

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

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

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

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

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?

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

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

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?

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

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

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

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

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.

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?

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

[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

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.

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

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

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?

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.

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

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.