1. Action์ ๋ฉ์๋๋ก ์ฒ๋ฆฌํ๊ธฐ
๐ ์ค์ต ๋งํฌ
๐ก Redux์ ๋ฌ๋ฆฌ ๋จ์ผ ์คํ ์ด๊ฐ ์๋๊ธฐ์, ์คํ ์ด ์ฌ๋ฌ ๊ฐ๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ฉด ๋จ
CounterStore, useCounterStore ๋ง๋ค๊ธฐ
// src/stores/ObjectStore.ts
type Listener = () => void;
export default class ObjectStore {
private listeners = new Set<Listener>();
addListener(listener: Listener) {
this.listeners.add(listener);
}
removeListener(listener: Listener) {
this.listeners.delete(listener);
}
protected publish() {
this.listeners.forEach((listener) => listener());
}
}
// src/stores/CounterStore.ts
import {singleton} from 'tsyringe';
import ObjectStore from './ObjectStore';
@singleton()
export default class CounterStore extends ObjectStore {
count = 0;
/**
* ์นด์ดํธ๋ฅผ ์ฆ๊ฐ์ํจ๋ค.
*/
increase(step = 1) {
this.count += step;
this.publish();
}
/**
* ์นด์ดํธ๋ฅผ ๊ฐ์์ํจ๋ค.
*/
decrease() {
this.count -= 1;
this.publish();
}
}
// src/hooks/useObjectStore.ts
import {useEffect} from 'react';
import useForceUpdate from './useForceUpdate';
import ObjectStore from '../stores/ObjectStore';
export default function useObjectStore<T extends ObjectStore>(store: T): T {
const forceUpdate = useForceUpdate();
useEffect(() => {
store.addListener(forceUpdate);
return () => store.removeListener(forceUpdate);
}, [store, forceUpdate]);
return store;
}
// src/hooks/useCounterStore.ts
import {container} from 'tsyringe';
import useObjectStore from './useObjectStore';
import CounterStore from '../stores/CounterStore';
export default function useCounterStore() {
const store = container.resolve(CounterStore);
return useObjectStore(store);
}
2. usestore-ts
usestore-ts
๐ ์ค์ต ๋งํฌ
React state management library
๐ก ๋ด๋ถ์ ์ผ๋ก ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํดํ๊ณ ์ฌ์ฉํ ๊ฒ
ํ์
์คํฌ๋ฆฝํธ๋ฅผ ์ ๋ชจ๋ฅธ๋ค๋ฉด ์ด๋ ค์ธ ์ ์์
๋จ์๊ฒ ์ค๋ช
ํ ๋ ๋ด๋ถ์ ์ผ๋ก๋ ์ด๋ฐ ๋์์ ํ๊ณ ์์ผ๋ฉฐ ์ด๋ค ๋ฐฉ์์ผ๋ก ์๋ํ๋์ง ์๊ธฐํ ์ ์์ด์ผ ํจ
์ฌ์ฉ ๋ฐฉ๋ฒ
ํจํค์ง ์ค์น
npm install usestore-ts
tsconfig.json
ํ์ผ decorators ์ฌ์ฉ ์ต์
๋ณ๊ฒฝ
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
Store ์์ฑ
// CounterStore.ts
import {singleton} from 'tsyringe';
import {Store, Action} from 'usestore-ts';
@singleton()
@Store()
export default class CounterStore {
count = 0;
@Action()
increase(step = 1) {
this.count += step;
}
@Action()
decrease() {
this.count -= 1;
}
}
Custom Hook ์์ฑ
// useCounterStore.ts
import {container} from 'tsyringe';
import {useStore} from 'usestore-ts';
import CounterStore from '../stores/CounterStore';
export default function useCounterStore() {
const store = container.resolve(CounterStore);
return useStore(store);
}
์ฌ์ฉ ๋ฐฉ๋ฒ ์์
const [{ count }, store] = useStore(counterStore);
์ฒซ๋ฒ์งธ ์ธ์๋ก ์คํ ์ด์ ๊ฐ, ๋๋ฒ์งธ ์ธ์๋ก ์คํ ์ด ์์ฒด๊ฐ ๋ฐํ
๋ถ๋ฌ์์ ์ฌ์ฉํ๊ธฐ
// Counter.tsx
export default function Counter() {
const [{count}] = useCounterStore();
// ๋ด์ฉ
}
// CounterControl.tsx
export default function CounterControl() {
const [, store] = useCounterStore();
// ๋ด์ฉ
}
๋น๋๊ธฐ ํจ์์ @Action ์ถ๊ฐ
โ ๏ธ ๋น๋๊ธฐ ํจ์์ @Action์ ๋ถ์ด๋ฉด ๋ค๋ฅด๊ฒ ์๋ํ ์ ์๋ค๋ ์ ์ ์ฃผ์
๋ณ๋์ ์ก์
์ ๋ง๋ค๋ฉด ์ ๊ฒฝ ์ธ ๋ถ๋ถ์ด ์ค์ด๋ฆ
start, complete ๋ถ์ ๊ฒ์ด ์ก์
@singleton()
@Store()
class PostStore {
posts: Post[] = [];
// ์ฌ๊ธฐ์ @Action() ๋ฅผ ๋ถ์ผ ๊ฒฝ์ฐ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฆฌํดํด์ publish
async fetchPosts() {
this.startLoading();
const posts = await fetchPosts();
this.completeLoading(posts);
}
@Action()
startLoading() {
this.posts = [];
}
@Action()
completeLoading(posts: Post[]) {
this.posts = posts;
}
}
Immer
Immer
๋ถ๋ณ์ฑ์ ์ ์งํด์ฃผ๋ ์ฝ๋๋ฅผ ํธ๋ฆฌํ ๋ฐฉ์์ผ๋ก ์์
ํ ์ ์๊ฒ ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์ค๋ธ์ ํธ ์ ์ฒด๋ฅผ ๋ณต์ฌํด์ ์ฃผ๊ณ , ๊ทธ๊ฒ์ ์ด์ฉํ ์ ์๋๋ก ๋์ด ์์
Redux Toolkit ์๋ ๊ธฐ๋ณธ์ผ๋ก ์ ์ฉ
import produce from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
3. useSyncExternalStore
useSyncExternalStore
FECONF 2022 - ์ํ๊ด๋ฆฌ ์ด ์ ์์ ๋๋ด๋ฌ ์๋ค
external store๋ฅผ ๊ตฌ๋
ํ ์ ์๋ React Hook
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot ?)