이는 상태관리 컨벤션을 확립하기 위함이다.
구현체는 아래와 같다.
import { Atom, WritableAtom, atom } from 'jotai';
export abstract class AtomManager<T> {
public initialState: T;
protected atom: WritableAtom<T, any, void>;
constructor(initialState: T) {
this.initialState = initialState;
this.atom = atom(this.initialState);
}
abstract selectors: {
[K in keyof Partial<T>]: Atom<T[K]>;
};
abstract actions: {
[key: string]: WritableAtom<T | null, any, void>;
};
}
각 selectors는 getter atom을 프로퍼티로 갖는다.
각 actions는 setter atom을 프로퍼티로 갖는다.
class CartManager extends AtomManager<Cart> {
constructor(initialState: Cart) {
super(initialState);
}
/* Selectors */
public selectors = {
items: atom((get) => get(this.atom).items),
};
/* Actions */
public actions = {
add: atom(
null,
(get, set, { amount, product }: { amount: number; product: Product }) => {
const { items } = get(this.atom);
// ...codes
}
),
delete: atom(null, (get, set, product: Product) => {
const { items } = get(this.atom);
// ...codes
}),
};
}
const initialData: Cart = {
items: [],
};
export const cartManager = new CartManager(initialData);
useManager은 두 가지 역할을 수행해야한다.
- 추상클래스로 추상화한 field(selectors, actions)가 가진 method Type들을 동적으로 추론해 반환해야한다.
- atom을 useAtomValue 등의 훅을 사용하여 state로 변환해야 한다.
export const useManager = <T extends AtomManager<any>>(manager: T) => {
const selectors = Object.fromEntries(
Object.entries(manager.selectors).map(([key, atom]) => [
key,
useAtomValue(atom),
])
) as {
[P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
? V
: never;
};
const actions = Object.fromEntries(
Object.entries(manager.actions).map(([key, actionAtom]) => [
key,
useSetAtom(actionAtom),
])
) as {
[P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
any,
infer U,
void
>
? (param: U[0]) => void
: never;
};
return { selectors, actions };
};
현재 코드는 콜백함수 내부에서 커스텀훅을 호출하여, hook의 호출 순서를 보장받지 못한다.
export const useManager = <T extends AtomManager<any>>(manager: T) => {
const selectors = Object.fromEntries(
Object.entries(manager.selectors).map(([key, atom]) => [
key,
useAtomValue(atom), // 🚩 Error : Callback 내부에서 커스텀훅 호출
])
) as {
[P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
? V
: never;
};
const actions = Object.fromEntries(
Object.entries(manager.actions).map(([key, actionAtom]) => [
key,
useSetAtom(actionAtom), // 🚩 Error : Callback 내부에서 커스텀훅 호출
])
) as {
[P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
any,
infer U,
void
>
? (param: U[0]) => void
: never;
};
return { selectors, actions };
};
커스텀훅은 최상위 Layer에서만 호출되어야한다.
Proxy를 써서 파훼를 시도했지만 역시 실패하였다.
고차함수는 답을 알고있다. 커링을 응용해 props를 전달하는 함수를 반환하는 고차함수를 작성해주자.
최상위 layer에서 Hook을 사용할 수 있도록 함수로 래핑하여 반환해주자.
import { Atom, WritableAtom, useAtomValue, useSetAtom } from 'jotai';
import { AtomManager } from '@/Model/manager/atomManager';
const createUseSelector = <T>(atom: Atom<T>) => {
return () => useAtomValue(atom); // ✅
};
const createUseAction = (atom: WritableAtom<any, any, void>) => {
return () => useSetAtom(atom); // ✅
};
export const useManager = <T extends AtomManager<any>>(manager: T) => {
const selectors = Object.fromEntries(
Object.entries(manager.selectors).map(([key, atom]) => [
key,
createUseSelector(atom)(), // ✅
])
) as {
[P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
? V
: never;
};
const actions = Object.fromEntries(
Object.entries(manager.actions).map(([key, actionAtom]) => [
key,
createUseAction(actionAtom)(), // ✅
])
) as unknown as {
[P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
any,
infer U,
void
>
? (param: U[0]) => void
: never;
};
return { selectors, actions };
};
개선하여 @pengoose/jotai V1.1.4 배포! 😆👍
NextStep 그리고 소인성님
감사합니다.