
Une petite solution de gestion de la gestion des états de Bearbones rapide et évolutive en utilisant des principes de flux simplifiés. A une API confortable basée sur des crochets, n'est pas Boilerplatey ou d'opinion.
Ne le négligez pas parce que c'est mignon. Il a tout à fait les griffes, beaucoup de temps a été consacré à faire face à des pièges communs, comme le problème des enfants zombies redouté, réagir la concurrence et la perte de contexte entre les rendus mixtes. C'est peut-être le seul manager de l'État dans l'espace React qui obtient tout cela.
Vous pouvez essayer une démo en direct ici.
npm install zustand Votre magasin est un crochet! Vous pouvez y mettre n'importe quoi: primitives, objets, fonctions. L'état doit être mis à jour de manière immuable et la fonction set fusionne l'état pour l'aider.
import { create } from 'zustand'
const useBearStore = create ( ( set ) => ( {
bears : 0 ,
increasePopulation : ( ) => set ( ( state ) => ( { bears : state . bears + 1 } ) ) ,
removeAllBears : ( ) => set ( { bears : 0 } ) ,
} ) ) Utilisez le crochet n'importe où, aucun fournisseur n'est nécessaire. Sélectionnez votre état et le composant sera rendu sur les modifications.
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 >
}Vous pouvez, mais gardez à l'esprit que cela entraînera la mise à jour du composant sur chaque changement d'état!
const state = useBearStore ( ) Il détecte les modifications avec l'égalité stricte (ancien === nouveau) par défaut, ceci est efficace pour les choix d'état atomiques.
const nuts = useBearStore ( ( state ) => state . nuts )
const honey = useBearStore ( ( state ) => state . honey )Si vous souhaitez construire un seul objet avec plusieurs séances d'état à l'intérieur, similaire à MapStateToprops de Redux, vous pouvez utiliser USESHALLOW pour éviter les rerendeurs inutiles lorsque la sortie du sélecteur ne change pas en fonction de l'égalité peu profonde.
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
const useBearStore = create ( ( set ) => ( {
nuts : 0 ,
honey : 0 ,
treats : { } ,
// ...
} ) )
// 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 ) ) ) Pour plus de contrôle sur la rediffusion, vous pouvez fournir n'importe quelle fonction d'égalité personnalisée (cet exemple nécessite l'utilisation de createWithEqualityFn ).
const treats = useBearStore (
( state ) => state . treats ,
( oldTreats , newTreats ) => compare ( oldTreats , newTreats ) ,
) La fonction set a un deuxième argument, false par défaut. Au lieu de fusionner, il remplacera le modèle d'état. Faites attention à ne pas effacer les pièces sur lesquelles vous comptez, comme les 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 ) ,
} ) ) Il suffit d' set lorsque vous êtes prêt, Zustand ne se soucie pas si vos actions sont asynchrones ou non.
const useFishStore = create ( ( set ) => ( {
fishies : { } ,
fetch : async ( pond ) => {
const response = await fetch ( pond )
set ( { fishies : await response . json ( ) } )
} ,
} ) ) set permet set(state => result) , mais vous avez toujours accès à l'état en dehors de celui-ci via get .
const useSoundStore = create ( ( set , get ) => ( {
sound : 'grunt' ,
action : ( ) => {
const sound = get ( ) . sound
. . . Parfois, vous devez accéder à l'état de manière non réactive ou agir sur le magasin. Pour ces cas, le crochet résultant a des fonctions d'utilité attachées à son prototype.
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 )
. . . Si vous avez besoin de vous abonner avec un sélecteur, subscribeWithSelector le middleware Selon.
Avec ce middleware subscribe accepte une signature supplémentaire:
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 ,
} ) Le noyau Zustand peut être importé et utilisé sans la dépendance REACT. La seule différence est que la fonction de création ne renvoie pas de crochet, mais les utilitaires API.
import { createStore } from 'zustand/vanilla'
const store = createStore ( ( set ) => ... )
const { getState , setState , subscribe , getInitialState } = store
export default store Vous pouvez utiliser un magasin de vanille avec useStore Hook disponible depuis V4.
import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'
const useBoundStore = ( selector ) => useStore ( vanillaStore , selector )set ou get ne sont pas appliqués à getState et setState .
La fonction d'abonnement permet aux composants de se lier à une portion d'état sans forcer la réapprovisionnement sur les modifications. Meilleur combinez-le avec UseEffect pour un désabonnement automatique sur Unmount. Cela peut avoir un impact drastique des performances lorsque vous êtes autorisé à muter directement la vue.
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 )
) , [ ] )
. . . Réduire les structures imbriquées est fastidieuse. Avez-vous essayé 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 ( )Alternativement, il existe d'autres solutions.
Vous pouvez persister les données de votre magasin à l'aide de tout type de stockage.
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
} ,
) ,
)Voir la documentation complète de ce middleware.
IMMER est également disponible en middleware.
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create (
immer ( ( set ) => ( {
bees : 0 ,
addBees : ( by ) =>
set ( ( state ) => {
state . bees += by
} ) ,
} ) ) ,
) 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 } )Ou, utilisez simplement notre logiciel-middle Redux. Il câble votre réducteur principal, définit l'état initial et ajoute une fonction d'expédition à l'état lui-même et à l'API Vanilla.
import { redux } from 'zustand/middleware'
const useGrumpyStore = create ( redux ( reducer , initialState ) ) Installez l'extension Redux Devtools Chrome pour utiliser le middleware 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 ) ) )Une connexion Redux Devtools pour plusieurs magasins
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 } )L'attribution de différents noms de connexion séparera les magasins dans Redux Devtools. Cela aide également à regrouper différents magasins en connexions Redux Devtools séparées.
Devtools prend la fonction du magasin comme son premier argument, éventuellement, vous pouvez nommer le magasin ou configurer les options de sérialisation avec un deuxième argument.
Nom Store: devtools(..., {name: "MyStore"}) , qui créera une instance distincte nommée "MyStore" dans les Devtools.
Options de sérialisation: devtools(..., { serialize: { options: true } }) .
Devtools ne journalisera que les actions de chaque magasin séparé contrairement à un magasin Reducers Reducers Typique Reducers . Voir une approche pour combiner les magasins # 163
Vous pouvez enregistrer un type d'action spécifique pour chaque fonction set en passant un troisième paramètre:
const useBearStore = create ( devtools ( ( set ) => ( {
...
eatFish : ( ) = > set (
( prev ) => ( { fishes : prev . fishes > 1 ? prev . fishes - 1 : 0 } ) ,
undefined ,
'bear/eatFish'
) ,
...Vous pouvez également enregistrer le type de l'action avec sa charge utile:
...
addFishes : ( count ) => set (
( prev ) => ( { fishes : prev . fishes + count } ) ,
undefined ,
{ type : 'bear/addFishes' , count , }
) ,
... Si un type d'action n'est pas fourni, il est par défaut "anonyme". Vous pouvez personnaliser cette valeur par défaut en fournissant un paramètre anonymousActionType :
devtools ( ... , { anonymousActionType : 'unknown' , ... } ) Si vous souhaitez désactiver Devtools (sur la production par exemple). Vous pouvez personnaliser ce paramètre en fournissant le paramètre enabled :
devtools ( ... , { enabled : false , ... } ) Le magasin créé avec create ne nécessite pas de fournisseurs de contexte. Dans certains cas, vous pouvez utiliser des contextes pour l'injection de dépendance ou si vous souhaitez initialiser votre magasin avec des accessoires à partir d'un composant. Parce que le magasin normal est un crochet, le passer comme une valeur de contexte normale peut violer les règles des crochets.
La méthode recommandée disponible depuis V4 est d'utiliser le magasin de vanille.
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 )
. . . L'utilisation de base dactylographiée ne nécessite rien de spécial sauf pour l'écriture create<State>()(...) au lieu de 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' ,
} ,
) ,
) ,
)Un guide dactylographié plus complet est ici.
Certains utilisateurs peuvent vouloir étendre l'ensemble de fonctionnalités de Zustand qui peut être effectué à l'aide de bibliothèques tierces fabriquées par la communauté. Pour plus d'informations sur les bibliothèques tierces avec Zustand, visitez le DOC.