Redux 따라하기
dispatch를 받고 selector를 이용해서 상태를 얻을 수 있다.
dispatch에 액션을 던져준다.(객체로)
external store는 순수한 자바스크립트에 가깝기 때문에 테스트하기 쉽다.
쉬린지는 객체만 넘겨줄수가 있다.
실제로 리덕스 써보면 컨텍스트로 스토어를 만들어서 넘겨줄 수 있다.
강의 에제
Store 폴더
BaseStore.ts
export type Action<Payload> = {
type: string;
payload?: Payload;
};
type Reducer<State, Payload> = (state: State, action: Action<Payload>) => State;
type Listener = () => void;
type Reducers<State> = Record<string, Reducer<State, any>>;
export default class BaseStore<State> {
state: State;
reducer: Reducer<State, any>;
listeners = new Set<Listener>();
constructor(initialState: State, reducers: Reducers<State>) {
this.state = initialState;
this.reducer = (state: State, action: Action<any>) => {
const reducer = Reflect.get(reducers, action.type);
if (!reducer) {
return state;
}
return reducer(state, action);
};
}
// Dispatch는 액션을 받는다.
dispatch(action: Action<any>) {
// Reducer를 실행해준다. 그러면 새로운 state가 나옴
this.state = this.reducer(this.state, action);
// 그러고 나서 publish하면 된다.
this.publish();
}
publish() {
this.listeners.forEach(listener => {
listener();
});
}
addListener(listener: () => void) {
this.listeners.add(listener);
}
removeListener(listener: () => void) {
this.listeners.delete(listener);
}
}
store.ts
import { singleton } from "tsyringe";
import BaseStore, { type Action } from "./BaseStore";
// Type State = {
// count: number;
// };
const initialState = {
count: 0,
// 상태를 추가한다면?
name: "Tester",
};
export type State = typeof initialState;
const reducers = {
increase(state: State, action: Action<number>) {
return {
...state,
count: state.count + (action.payload ?? 1),
};
},
decrease(state: State, action: Action<number>) {
return {
...state,
count: state.count - (action.payload ?? 1),
// Name: `${state.name}.`,
};
},
};
export function increase(step?: number) {
return { type: "increase", payload: step };
}
export function decrease(step?: number) {
return { type: "decrease", payload: step };
}
// @쓰는것을 decorate라고 함함
@singleton()
export default class Store extends BaseStore<State> {
constructor() {
super(initialState, reducers);
}
}
hooks 폴더
useDispatch.ts
import { type Action } from "../stores/BaseStore";
import { container } from "tsyringe";
import Store from "../stores/Store";
export default function useDispatch() {
const store = container.resolve(Store);
return (action: Action<any>) => {
store.dispatch(action);
};
}
useForceUpdate.ts
import { useState, useCallback } from "react";
export default function useForceUpdate() {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
}
useSelector.ts
import Store, { type State } from "../stores/Store";
import { container } from "tsyringe";
import useForceUpdate from "./useForceUpdate";
import { useEffect, useRef } from "react";
type Selector<T> = (state: State) => T;
export default function useSelector<T>(selector: Selector<T>): T {
const store = container.resolve(Store);
const state = useRef(selector(store.state));
const forceUpdate = useForceUpdate();
useEffect(() => {
const update = () => {
// 특정 조건이 맞으면 forceUpdate
const newState = selector(store.state);
// Selector의 결과가 객체인 경우 처리가 필요하다.
if (newState !== state.current) {
forceUpdate();
state.current = newState;
}
};
store.addListener(update);
return () => {
store.removeListener(forceUpdate);
};
}, [store, forceUpdate]);
return selector(store.state);
}
Components 폴더
CountControl.tsx
import useDispatch from "../hooks/useDispatch";
import useSelector from "../hooks/useSelector";
import { decrease, increase, type State } from "../stores/Store";
// UI : ui는 비교적 자주 바뀌는데 비즈니스 로직이 엮여있다면 테스트가 터져서 유지보수가 힘듬.
export default function CounterControl() {
// Action을 넘겨주기 위한 dispatch는 이렇게 받고
const dispatch = useDispatch();
// 상태는 useSelector를 통해 받는다.
const count = useSelector((state: State) => state.count);
return (
<div>
<p>{count}</p>
<button
type="button"
onClick={() => {
dispatch(increase());
}}
>
Increase
</button>
<button
type="button"
onClick={() => {
dispatch(increase(10));
}}
>
Increase 10
</button>
<button
type="button"
onClick={() => {
dispatch(decrease());
}}
>
Decrease
</button>
<button
type="button"
onClick={() => {
dispatch(decrease(10));
}}
>
Decrease 10
</button>
</div>
);
}
Counter.tsx
import useSelector from "../hooks/useSelector";
// UI : ui는 비교적 자주 바뀌는데 비즈니스 로직이 엮여있다면 테스트가 터져서 유지보수가 힘듬.
export default function Counter() {
const count = useSelector((state) => state.count);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
NameCard.tsx
import { useEffect } from "react";
import useSelector from "../hooks/useSelector";
// UI : ui는 비교적 자주 바뀌는데 비즈니스 로직이 엮여있다면 테스트가 터져서 유지보수가 힘듬.
export default function NameCard() {
const name = useSelector((state) => state.name);
// 이름 상태는 안바뀌는데 재렌더링이 되는 문제
useEffect(() => {
console.log("render NameCard");
});
return (
<div>
<p>Name: {name}</p>
</div>
);
}
Last updated