Gathering detailed insights and metrics for @lukesmurray/zustand-scoped
Gathering detailed insights and metrics for @lukesmurray/zustand-scoped
Gathering detailed insights and metrics for @lukesmurray/zustand-scoped
Gathering detailed insights and metrics for @lukesmurray/zustand-scoped
npm install @lukesmurray/zustand-scoped
Typescript
Module System
Node Version
NPM Version
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
28
Created scoped (nested) zustand stores which can be called with initial data. Typesafe and supports (almost all) zustand middleware.
This package solves two issues in Zustand. Combining stores ( #291, #161, #163, #178 ) and initializing stores with data ( #82, #552 ).
1# npm 2npm install @lukesmurray/zustand-scoped 3 4# yarn 5yarn add @lukesmurray/zustand-scoped
The createScopedStore
function is like create
from zustand
but it adds an extra function (initialData) => ...
before the (set, get) => ...
part.
The return value is a store factory
! Pass initial data to it to create a new store.
1import { createScopedStore } from "@lukesmurray/zustand-scoped"; 2 3type BearState = { 4 bears: number; 5 increasePopulation: () => void; 6 removeAllBears: () => void; 7}; 8 9type BearInitialData = { 10 bears: number; 11}; 12 13// define a store factory 14const createBearStore = createScopedStore<BearState, BearInitialData>()( 15 (initialData) => 16 (set, get) => ({ 17 bears: initialData.bears, 18 increasePopulation: () => 19 set((state) => ({ ...state, bears: state.bears + 1 })), 20 removeAllBears: () => set({ ...get(), bears: 0 }), 21 }) 22); 23 24// create a hook by passing initial data. 25const useBearStore = createBearStore({ bears: 0 });
1function Bears({ bears }: { bears: number }) { 2 const [useBearStore] = useState(() => createBearStore({ bears })); 3 const bearCount = useBearStore((state) => state.bears); 4 const increasePopulation = useBearStore((state) => state.increasePopulation); 5 return ( 6 <div> 7 <h1>I see {bearCount} bears...</h1> 8 <button onClick={increasePopulation}>Add One</button> 9 </div> 10 ); 11}
createScopedStore
returns a function that can be called directly to get a react hook for your store.
But it also returns two unique properties.
You can access a vanilla store by calling createBearStore.store(initialData)
1const vanillaStore = createBearStore.store({ bears: 0 }); 2 3// use the vanilla store 4const { getState, setState, subscribe } = vanillaStore;
You can create a scoped store by called createBearStore.scoped(setOverride, getOverride, initialData)
.
See the section on scoped stores below.
If all the properties in initial data are optional, you do not need to pass initial data to the store creator functions. However, if any parameter is non-optional, you must pass initial data.
1// all properties optional 2type BearInitialData = { 3 bears?: number; 4}; 5 6// create hook without passing initial data. 7createBearStore() 8 9// create vanilla without passing initial data. 10createBearStore.store()
Scoped Stores let you override the set
and get
method for child stores.
The inspiration came from this issue comment.
The overriden set
function receives the child state but must update the parent state.
We'll start by creating a todo store.
The only change is we add the stateReq
middleware which enforces that set
cannot be called with partial state or return partial state.
This is extremely helpful since you often need access to the entire child state to know which child to update from the parent.
1import { createScopedStore, stateReq } from "@lukesmurray/zustand-scoped"; 2 3interface TodoState { 4 // unique id so we can find the todo in the parent store. 5 id: string; 6 checked: boolean; 7 toggleDone: () => void; 8} 9 10interface TodoInitialData { 11 // the initial data should contain enough information to find the todo in the parent store. 12 // so we include the id and make it required! 13 id: string; 14 checked?: boolean; 15} 16 17const createTodoStore = createScopedStore<TodoState, TodoInitialData>()( 18 (initialData) => 19 // if you plan to nest a store. Use the stateReq middleware. 20 stateReq((set) => ({ 21 id: initialData.id, 22 checked: initialData.checked ?? false, 23 toggleDone: () => set((state) => ({ ...state, checked: !state.checked })), 24 })) 25);
Next we'll define the parent store state. The parent store simply contains the child store's state.
1interface AppState { 2 // note that the nested store is stored as State, not as StoreApi<State>. 3 todos: TodoState[]; 4 addTodo: (todo: TodoInitialData) => void; 5 removeTodo: (id: string) => void; 6}
Now we'll create the parent store.
The most complicated part of this function is defining the helper function to create the nested stores.
If you have questions about how createNestedTodoStore
works please open an issue!
Fundamentally its the same pattern as the much shorter example in this issue comment just with typesafety.
1const createAppStore = createScopedStore<AppState>()(() => 2 stateReq((set, get) => { 3 // define a helper function to create nested stores. 4 const createNestedTodoStore = (todoInitialData: TodoInitialData) => { 5 // define a selector to get the todo from the parent store. 6 const selectTodo = (state: AppState) => 7 state.todos.find((t) => t.id === todoInitialData.id)!; 8 9 // define a helper function to resolve the parameter passed 10 // to the todo's set function into the next todo state. 11 const resolveTodo = ( 12 partial: TodoState | ((state: TodoState) => TodoState) 13 ) => { 14 return typeof partial === "function" 15 ? partial(selectTodo(get())) 16 : partial; 17 }; 18 19 // create the new set function for the nested store. 20 const setTodo: Parameters<typeof createTodoStore.scoped>[0] = ( 21 currentTodoStateOrUpdater 22 ) => { 23 // resolve the next state or updater into the next todo state. 24 const nextTodoState = resolveTodo(currentTodoStateOrUpdater); 25 26 // apply a standard immutable update to the parent store. 27 return set((state) => ({ 28 ...state, 29 todos: state.todos.map((todo) => 30 todo.id === nextTodoState.id ? nextTodoState : todo 31 ), 32 })); 33 }; 34 35 // create the new get function for the nested store. 36 // we can use the selector we defined above. 37 const getTodo: Parameters<typeof createTodoStore.scoped>[1] = () => 38 selectTodo(get()); 39 40 // create the nested store. 41 return createTodoStore.scoped(setTodo, getTodo, todoInitialData); 42 }; 43 44 return { 45 todos: [], 46 addTodo: (todoInitialData) => 47 set((state) => ({ 48 ...state, 49 todos: [ 50 ...state.todos, 51 // use the helper function to create the nested store. 52 createNestedTodoStore(todoInitialData), 53 ], 54 })), 55 removeTodo: (id) => 56 set((state) => ({ 57 ...state, 58 todos: state.todos.filter((t) => t.id !== id), 59 })), 60 }; 61 }) 62);
The helper createScopedHook
constructs a hook that can select from the nested TodoStore
.
We pass a selector to createScopedHook
to select a todo from the AppStore
.
The selector must take a single arugment.
That argument becomes the first parameter of the returned hook.
1const useAppStore = createAppStore(); 2 3// use createScopedHook to create a hook that accesses the todo store 4const useTodoStore = createScopedHook( 5 useAppStore, 6 // select a todo by id. The single argument to this function becomes 7 // the first argument of the returned hook. 8 (id: string) => (state) => state.todos.find((t) => t.id === id)! 9); 10 11// whether the first todo is checked 12useTodoStore("1", (state) => state.checked) 13// whether the second todo is checked 14useTodoStore("2", (state) => state.checked) 15 16function Todo({ todoId }: { todoId: string }) { 17 const checked = useTodoStore(todoId, (state) => state.checked); 18 19 return <div>The todo is {checked ? "checked" : "not checked"}</div>; 20}
We can pass a default argument to createScopedHook
to automatically select a specific piece of nested state.
The returned hook no longer takes the "selector argument"
1// pass a third argument to always select the todo with id "1". 2const useFirstTodoStore = createScopedHook( 3 useAppStore, 4 (id: string) => (state) => state.todos.find((t) => t.id === id)!, 5 "1" 6); 7 8function FirstTodo() { 9 // you can use the hook without passing an id. 10 const checked = useFirstTodoStore((state) => state.checked); 11 12 return <div>The todo is {checked ? "checked" : "not checked"}</div>; 13}
Finally we can use the app store hook without any changes.
1function App() { 2 const addTodo = useAppStore((state) => state.addTodo({ id: "1" })); 3}
The stateReq middleware enforces that all state is returned by the set
function.
This is helpful in nested store since you often need to find the element to update based on an identifying property such as an id
.
If set
returns a Partial
state, then you have no guarantee that the identifying property is included.
1import { createScopedStore, stateReq } from "@lukesmurray/zustand-scoped"; 2 3const createTodoStore = createScopedStore<TodoState, TodoInitialData>()( 4 (initialData) => 5 stateReq((set) => ({ 6 id: initialData.id, 7 checked: initialData.checked ?? false, 8 9 // ✅ we returned the entire state from set using {...state} 10 toggleDone: () => set((state) => ({ ...state, checked: !state.checked })), 11 12 // ❌ we only returned partial state {checked}. 13 // toggleDone: () => set((state) => ({ checked: !state.checked })), 14 })) 15);
The devtoolsReq middleware enforces that action
is passed to every set method.
This can be helpful if you want to enforce that all actions are named, which makes debugging large stores significantly easier.
1import { createScopedStore, devtoolsReq } from "@lukesmurray/zustand-scoped"; 2 3const createTodoStore = createScopedStore<TodoState, TodoInitialData>()( 4 (initialData) => 5 devtoolsReq((set, get) => ({ 6 id: initialData.id, 7 checked: initialData.checked ?? false, 8 // ✅ We pass the action as a third argument to set. 9 toggleDone: () => 10 set({ ...get(), checked: !get().checked }, false, "toggleDone"), 11 12 // ❌ We did not pass the action as a third argument to set. 13 // toggleDone: () => 14 // set({ ...get(), checked: !get().checked }, false), 15 })) 16);
If you have questions check out the examples in the examples folder.
stateReq
it does not work with immer
middleware. The two middlewares are contradictory. stateReq
requires that set
returns the store's state. immer
allows set
to return void.subscribe
and destroy
do not work inside of stores created with factory.scoped
. This only affects you if you call subscribe
or destroy
in your createStore
function which I've never seen.devtoolsReq
— ["zustand-scoped/devtoolsReq", never]
stateReq
— ["zustand-scoped/stateReq", never]
Install dependencies with yarn install
.
Lint files with yarn lint .
.
Run tests with yarn test
.
Run tests with coverage with yarn test:coverage
.
npm pack
and check that the correct files are includedyarn lint . && yarn build && npm publish --access public
No vulnerabilities found.
No security vulnerabilities found.