3. useSyncExternalStore
useSyncExternalStore란?
useSyncExternalStore는 외부 스토어를 구독할 수 있는 React훅이다.
전역상태 관리를 할 수 있으며, 각 개별적인 상태에 대한 저장, 업데이트가 가능하다.
사용하기
/src/store/BaseStore.ts
export type Listener = () => void;
class BaseStore<Snapshot> {
protected listeners = new Set<Listener>();
snapshot = {} as Snapshot;
addListener(listener: Listener): void {
this.listeners.add(listener);
}
removeListener(listener: Listener): void {
this.listeners.delete(listener);
}
publish() {
this.listeners.forEach((listener) => listener());
}
getSnapshot() {
return this.snapshot;
}
getListener() {
return this.listeners;
}
}
export default BaseStore;
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
/src/store/BaseStore.test.ts
import BaseStore, { Listener } from './BaseStore';
describe('BaseStore', () => {
type AnySnapshot = {
obj: {
id: number,
name: string,
},
};
let baseStore: BaseStore<AnySnapshot>;
let listener1: Listener;
let listener2: Listener;
beforeEach(() => {
baseStore = new BaseStore();
listener1 = jest.fn();
listener2 = jest.fn();
});
it('should add and remove listeners correctly', () => {
baseStore.addListener(listener1);
baseStore.addListener(listener2);
expect(baseStore.getListener().size).toBe(2);
baseStore.removeListener(listener1);
expect(baseStore.getListener().size).toBe(1);
expect(baseStore.getListener().has(listener1)).toBe(false);
expect(baseStore.getListener().has(listener2)).toBe(true);
});
});
/src/store/CartStore.ts
import { singleton } from 'tsyringe';
import BaseStore from './BaseStore';
import CartModel from '../models/CartModel';
import CartItemModel from '../models/CartItemModel';
import RestaurantModel from '../models/RestaurantModel';
import MenuItemModel from '../models/MenuItemModel';
export type CartStoreSnapshot = {
cartItems: CartItemModel[];
};
@singleton()
class CartStore extends BaseStore<CartStoreSnapshot> {
private cart = new CartModel();
constructor() {
super();
this.takeSnapshot();
}
takeSnapshot() {
this.snapshot = {
cartItems: this.cart.cartItems,
};
}
private update() {
this.takeSnapshot();
this.publish();
}
setOrderType(orderType: string) {
this.cart = this.cart.setOrderType(orderType);
}
addItem({
restaurant,
menuItem,
quantity,
}: {
restaurant: RestaurantModel;
menuItem: MenuItemModel;
quantity: number;
}) {
this.cart = this.cart.upsertItem({ restaurant, menuItem, quantity });
this.update();
}
removeItem({ menuId }: { menuId: number }) {
this.cart = this.cart.deleteItem({ menuId });
this.update();
}
clear() {
this.cart = this.cart.clearItems();
this.update();
}
// Properties
totalItemNum(): number {
return this.cart.totalItemNum();
}
totalPrice(): number {
return this.cart.totalPrice();
}
formattedTotalPrice(): string {
return this.cart.formattedTotalPrice();
}
getCart() {
return this.cart;
}
}
export default CartStore;
/src/hooks/useCartStore.ts
import { useSyncExternalStore } from 'react';
import CartStore, { CartStoreSnapshot } from '../stores/CartStore';
const cartStore = new CartStore();
function useCartStore(): [CartStoreSnapshot, CartStore] {
const snapshot: CartStoreSnapshot = useSyncExternalStore(
(onStoreChange) => {
cartStore.addListener(onStoreChange);
return () => cartStore.removeListener(onStoreChange);
},
() => cartStore.getSnapshot(),
);
return [snapshot, cartStore];
}
export default useCartStore;
선호이유
리엑트에서 제공하는 훅을 사용한다.
리턴 타입을 명시적으로 지정할 수 있다.
테스트코드 작성에 용이하다.
Last updated