나중에정리해라
Zustand store 설계
배열에서 주의할 점은 이겁니다.
const completedTodos = useTodoStore((state) => state.todos.filter((todo) => todo.done), );
filter는 매번 새 배열을 만듭니다.
[] === [] // false
그래서 unrelated state 변경에도 selector가 다시 실행되면서 새 배열을 반환하면 리렌더링될 수 있습니다.
이럴 때는 방법이 몇 가지 있습니다.
첫째, shallow 비교를 씁니다.
import { useShallow } from 'zustand/react/shallow';
const completedTodos = useTodoStore( useShallow((state) => state.todos.filter((todo) => todo.done)), );
둘째, 더 작은 primitive를 구독합니다.
const completedCount = useTodoStore( (state) => state.todos.filter((todo) => todo.done).length, );
셋째, 리스트 렌더링에서는 id 목록과 item 구독을 나눕니다.
const todoIds = useTodoStore( useShallow((state) => state.todos.map((todo) => todo.id)), );
그리고 각 row에서 자기 todo만 구독합니다.
function TodoRow({ id }: { id: string }) { const todo = useTodoStore((state) => state.todos.find((todo) => todo.id === id), );
return <div>{todo?.title}</div>;
}
이렇게 하면 리스트 전체가 아니라 개별 row 단위로 리렌더링 범위를 줄일 수 있습니다.
structural sharing, 구조적 공유
Immer는 내부에서 draft를 Proxy로 감싸고, 어떤 부분이 실제로 변경됐는지 추적합니다
예를 들어:
const nextState = produce(prevState, (draft) => {
draft.user.name = 'Kim';
});
prevState !== nextState
prevState.user !== nextState.user
prevState.settings === nextState.settings
Zustand을 꼭 써야할 때 ?
Zustand는 내부적으로 external syncstore 개념을 사용한다.
- react context는 리액트 외부에서 상태값 변경이 불가능하다. 하지만 Zustand는 애초에 외부 스토어이므로 상태변경이 가능하다.
- render function + non render function간의 상태 동기화가 필요할 때 zustand를 사용한다.
- 대표적으로 언어 설정, market 설정 등 유즈케이스에서 리액트의 렌더러와 함수 포멧터(react 밖)의 상태 동기가 필요 할 때
use context selector 라이브러리 사용법 최소 정리
React 기본 useContext는 Provider의 value 참조가 바뀌면 해당 Context를 읽는 컴포넌트들이 넓게 리렌더링될 수 있다.
use-context-selector는 Context에서 필요한 조각만 selector로 구독하게 해준다.
import {
createContext,
useContextSelector,
} from 'use-context-selector';
type Store = {
count: number;
text: string;
increase: () => void;
setText: (text: string) => void;
};
const StoreContext = createContext<Store | null>(null);
Provider에서는 일반 Context처럼 값을 내려준다.
function StoreProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const value = useMemo(
() => ({
count,
text,
increase: () => setCount((prev) => prev + 1),
setText,
}),
[count, text],
);
return (
<StoreContext.Provider value={value}>
{children}
</StoreContext.Provider>
);
}
Consumer에서는 전체 Context를 받지 않고 필요한 값만 고른다.
function Counter() {
const count = useContextSelector(StoreContext, (state) => state?.count);
const increase = useContextSelector(StoreContext, (state) => state?.increase);
return <button onClick={increase}>{count}</button>;
}
function TextInput() {
const text = useContextSelector(StoreContext, (state) => state?.text);
const setText = useContextSelector(StoreContext, (state) => state?.setText);
return (
<input
value={text ?? ''}
onChange={(event) => setText?.(event.target.value)}
/>
);
}
핵심은 selector 결과가 바뀐 컴포넌트만 리렌더링 대상이 된다는 점이다.
useContext(Context)
=> Provider value가 바뀌면 넓게 리렌더링될 수 있음
useContextSelector(Context, selector)
=> selector가 반환한 값이 바뀔 때만 리렌더링
주의할 점:
const state = useContextSelector(Context, (state) => ({
count: state.count,
text: state.text,
}));
selector에서 새 객체를 만들면 매번 새 참조가 될 수 있다.
가능하면 primitive나 기존 참조를 그대로 반환하는 selector를 사용한다.
const count = useContextSelector(Context, (state) => state.count);
const text = useContextSelector(Context, (state) => state.text);