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
끗
답글 남기기