내일배움캠프[4기_Reac트랙]/TIL

내일배움캠프 React트랙 58일차 회고 (2022.01.20)

ecoEarth 2023. 1. 23. 11:56

Why zustand over redux?

Why zustand over context?

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

zustand 설치

yarn add zustand

zustand 사용하기

First create a store

import { create } from "zustand";

const useBear = create((set) => ({
  number: 0,
  increaseNumber: () => set((state) => ({ number: state.number + 1 })),
  deleteNumber: () => set((state) => ({ number: 0 })),
}));

export { useBear };

Then bind your components, and that's it!

import "./App.css";
import { useBear } from "./store/store";

function App() {
  const { number, increaseNumber, deleteNumber } = useBear();
  return (
    <div>
      <div>{number}</div>
      <button onClick={increaseNumber}>추가</button>
      <button onClick={deleteNumber}>되돌리기</button>
    </div>
  );
}

export default App;

Persist middleware

The Persist middleware enables you to store your Zustand state in a storage (e.g., Localstorage , Asyncstorage , IndexedpB , etc.), thus persisting it's data.

Simple example

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', // unique name
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
    }
  )
)
import { create } from "zustand";
import "./App.css";
import { createJSONStorage, persist } from "zustand/middleware";
// import { useNumberStore } from "./stores/store";

function App() {
  const useNumberStore = create(
    persist(
      (set, get) => ({
        number: 0,
        addNumber: () => set({ number: get().number + 1 }),
        undoNumber: () => set({ number: get().number * 0 }),
      }),
      {
        name: "number_storage", // unique name
        storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used
      }
    )
  );
  const { number, addNumber, undoNumber } = useNumberStore();
  return (
    <div>
      <div>{number}</div>
      <button onClick={addNumber}>추가</button>
      <button onClick={undoNumber}>확인</button>
    </div>
  );
}

export default App;

Hydration and asynchronous storages

To explain what is the "cost" of asynchronous storages, you need to understand what is hydration.

In a nutshell, hydration is a process of retrieving persisted state from the storage and merging it with the current state.

The Persist middleware does two kinds of hydration: synchronous and asynchronous. If the given storage is synchronous (e.g., localStorage), hydration will be done synchronously. On the other hand, if the given storage is asynchronous (e.g., AsyncStorage), hydration will be done asynchronously (shocking, I know!).

But what's the catch? With synchronous hydration, the Zustand store will already have been hydrated at its creation. In contrast, with asynchronous hydration, the Zustand store will be hydrated later on, in a microtask.

Why does it matter? Asynchronous hydration can cause some unexpected behaviors. For instance, if you use Zustand in a React app, the store will not be hydrated at the initial render. In cases where your app depends on the persisted value at page load, you might want to wait until the store has been hydrated before showing anything. For example, your app might think the user is not logged in because it's the default, but in reality the store has not been hydrated yet.


API

Version: >=3.6.3

The Persist API enables you to do a number of interactions with the Persist middleware from inside or outside of a React component.

 

getOptions

Type: () => Partial<PersistOptions>

Returns: Options of the Persist middleware

For example, it can be used to obtain the storage name:

useBoundStore.persist.getOptions().name

 

setOptions

Type: (newOptions: Partial<PersistOptions>) => void

Changes the middleware options. Note that the new options will be merged with the current ones.

For instance, this can be used to change the storage name:

useBoundStore.persist.setOptions({
  name: 'new-name',
})

Or even to change the storage engine:

useBoundStore.persist.setOptions({
  storage: createJSONStorage(() => sessionStorage),
})

 

clearStorage

Type: () => void

Clears everything stored under the name key.

useBoundStore.persist.clearStorage()

 

rehydrate

Type: () => Promise<void>

In some cases, you might want to trigger the rehydration manually. This can be done by calling the rehydrate method.

await useBoundStore.persist.rehydrate()

 

hasHydrated

Type: () => boolean

This is a non-reactive getter to check if the storage has been hydrated (note that it updates when calling rehydrate).

useBoundStore.persist.hasHydrated()

 

onHydrate

Type: (listener: (state) => void) => () => void

Returns: Unsubscribe function

This listener will be called when the hydration process starts.

const unsub = useBoundStore.persist.onHydrate((state) => {
  console.log('hydration starts')
})

// later on...
unsub()

 

onFinishHydration

Type: (listener: (state) => void) => () => void

Returns: Unsubscribe function

This listener will be called when the hydration process ends.

const unsub = useBoundStore.persist.onFinishHydration((state) => {
  console.log('hydration finished')
})

// later on...
unsub()