Zustand를 도입을 하려고 하는데(코틀린 아님, 타입스크립트임) 아무래도 기존에 쓰던게 리덕스라서 리덕스에 너무 묶여있어서 Zustand를 으뜨케 써야할지 잘 몰랐는데 공식문서를 보고 간단하게 만들면 되겠다 싶었다.

사실 그냥 쓰라면 쓸 수 있을 것 같은데 이게 가면 갈수록 이럴거면 걍 useState 쓰고 말지 이런게 있어서 Context를 만드는 방법을 알아냈고 간단하게 동일로직만 쏙 뽑아서 쓰기로 했다.

// states/Provider.tsx
import {
  type PropsWithChildren,
  type Context,
  useRef
} from "react";
import type {StoreApi} from "zustand";

type ExtractStoreFromContext<TContext> = TContext extends Context<infer TStore extends StoreApi<unknown>> ? TStore : never

export function createStoreProvider<TContext, TStore = ExtractStoreFromContext<TContext>>(
  Context: Context<TStore | null>,
  storeHook: () => TStore,
) {
  return function Provider({children}: PropsWithChildren) {
    const store = storeHook()
    const storeRef = useRef<TStore>()

    if (!storeRef.current) {
      storeRef.current = store
    }

    return (
      <Context.Provider value={storeRef.current}>
        {children}
      </Context.Provider>
    )
  }
}

대충 store를 가지는 컨텍스트를 만들고 그 컨텍스트를 제공하는 Provider를 만드는 것이다.

다음과 같이 쓴다.

// states/Counter.ts
import {createStore, StoreApi} from "zustand";
import React, {useContext} from "react";
import {useStoreWithEqualityFn} from "zustand/traditional";
import {createStoreProvider} from "./Provider.tsx";

export interface CounterState {
  count: number;
  text: string;

  increment(): void

  decrement(): void;

  reset(): void;

  setText(text: string): void;
}

function useCounterStore() {
  return createStore<CounterState>(set => ({
    count: 0,
    text: "",
    increment: () => set(state => ({
      count: state.count + 1
    })),
    decrement: () => set(state => ({
      count: state.count - 1
    })),
    reset: () => set(() => ({
      count: 0
    })),
    setText(text: string) {
      set(() => ({
        text
      }))
    }
  }))
}

export const CounterContext = React.createContext<StoreApi<CounterState> | null>(null)

const CounterStoreProvider = createStoreProvider(CounterContext, useCounterStore)

export function useCounterStoreInContext<U>(selector: (state: CounterState) => U) {
  const store = useContext(CounterContext)
  if (!store) {
    throw new Error("Missing Context")
  }
  return useStoreWithEqualityFn(store, selector)
}
// App.tsx
import './App.css'
import {CounterStoreProvider,useCounterStoreInContext } from "./states/Counter.ts";
import {createRef, FormEvent} from "react";

function App() {
  return (
    <>
      <h1>With Provider</h1>
      <WithProvider />
    </>
  )
}

function WithProvider() {
  return (
    <CounterStoreProvider>
      <Counter />
      <Message />
    </CounterStoreProvider>
  )
}

function Counter() {
  const {count, increment, decrement, reset} = useCounterStoreInContext(state => state)

  return (
    <div>
      <h2>Count : {count}</h2>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

function Message() {
  const {text, setText} = useCounterStoreInContext(state => state)
  const inputRef = createRef<HTMLInputElement>()

  function updateText(e: FormEvent) {
    e.preventDefault()
    setText(inputRef.current?.value ?? "")
  }

  return (
    <div>
      <h2>Text : {text}</h2>
      <form onSubmit={updateText}>
        <input type="text" ref={inputRef} defaultValue={text}/>
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

export default App

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다