From fa4c9d8043c45aa0b802bf42d4ded0988278104f Mon Sep 17 00:00:00 2001 From: ldu1020 Date: Sun, 22 Oct 2023 14:55:20 +0900 Subject: [PATCH] =?UTF-8?q?chore(useSlice.ts):=20=F0=9F=A4=96options=20nam?= =?UTF-8?q?ing,=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContextSection/ContextSection.tsx | 57 +++++++++++++++-- .../context/hooks/useExamplePageHandlers.ts | 25 -------- .../context/hooks/useExamplePageState.ts | 27 -------- .../context/hooks/useExampleSlice.ts | 38 +++++++++++ .../context/useExamplePageContext.ts | 17 +++-- src/containers/Home/Home.tsx | 44 +------------ src/contexts/global/hooks/useGlobalState.ts | 2 +- src/hooks/useSlice.ts | 9 ++- src/utils/react/create-slice.ts | 56 ++++++----------- src/utils/react/types/reducer.ts | 63 +++++++++++++++++++ 10 files changed, 193 insertions(+), 145 deletions(-) delete mode 100644 src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExamplePageHandlers.ts delete mode 100644 src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExamplePageState.ts create mode 100644 src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExampleSlice.ts diff --git a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/ContextSection.tsx b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/ContextSection.tsx index b9916786..dd001861 100644 --- a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/ContextSection.tsx +++ b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/ContextSection.tsx @@ -13,18 +13,38 @@ import { import ContextColorPicker from './components/ContextColorPicker'; import StatefulColorPicker from './components/StatefulColorPicker'; +import { exampleSlice } from './context/hooks/useExampleSlice'; import { useExamplePageContext } from './context/useExamplePageContext'; import withExamplePageProvider from './hocs/withExamplePageProvider'; +const increaseFromOutOfComponent = () => { + exampleSlice.dispatch({ + type: 'INCREASE_VALUE', + payload: 1, + }); +}; + +const decreaseFromOutOfComponent = () => { + exampleSlice.dispatch({ + type: 'DECREASE_VALUE', + payload: 1, + }); +}; + const ContextSection = () => { const value = useExamplePageContext((ctx) => ctx.state.value); - const decreaseValue = useExamplePageContext( - (ctx) => ctx.handler.decreaseValue, - ); - const increaseValue = useExamplePageContext( - (ctx) => ctx.handler.increaseValue, + const dispatch = useExamplePageContext((ctx) => ctx.dispatch); + const accessAbleValue = useExamplePageContext( + (ctx) => ctx.accessAbleState.value, ); + const decreaseValue = () => { + dispatch({ type: 'DECREASE_VALUE', payload: 1 }); + }; + const increaseValue = () => { + dispatch({ type: 'INCREASE_VALUE', payload: 1 }); + }; + return ( { + + + + ExamplePage (컴포넌트 바깥에서 접근하기) + + + state 를 만들때 useSlice 훅의 access 옵션의 값이 global 일 경우 + 컴포넌트 바깥에서 접근이 가능합니다.
+ 단, 지역 컨텍스트나 지역 상태에서는 컴포넌트 언마운트시 state 가 + 초기화가 되지 않을 수 있으므로 전역상태에서만 사용하는 것 을 + 권장합니다. +
+ {`Current Context Value is`} + + + + {accessAbleValue} + + + +
+
{} - -export const useExamplePageHandlers = ({ - state, - dispatch, -}: useExamplePageHandlersParams) => { - const logExample = useCallback(() => { - console.log('example', { dispatch, state }); - }, [dispatch, state]); - - const increaseValue = useCallback(() => { - dispatch({ type: 'SET_VALUE', payload: state.value + 1 }); - }, [dispatch, state.value]); - - const decreaseValue = useCallback(() => { - dispatch({ type: 'SET_VALUE', payload: state.value - 1 }); - }, [dispatch, state.value]); - - return { logExample, increaseValue, decreaseValue }; -}; diff --git a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExamplePageState.ts b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExamplePageState.ts deleted file mode 100644 index b9dc286c..00000000 --- a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExamplePageState.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useReducer } from 'react'; - -import { createSlice } from '@/utils/react/create-slice'; - -type GlobalStateType = { - value: number; -}; - -const initialState: GlobalStateType = { - value: 0, -}; - -const { reducer } = createSlice({ - initialState, - reducers: { - RESET: () => initialState, - SET_VALUE: (state, payload: number) => { - state.value = payload; - }, - }, -}); - -export const useExamplePageState = () => { - const [state, dispatch] = useReducer(reducer, initialState); - - return { state, dispatch }; -}; diff --git a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExampleSlice.ts b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExampleSlice.ts new file mode 100644 index 00000000..907d1aaf --- /dev/null +++ b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/hooks/useExampleSlice.ts @@ -0,0 +1,38 @@ +import { useSlice } from '@/hooks/useSlice'; + +import { createSlice } from '@/utils/react/create-slice'; + +type GlobalStateType = { + value: number; +}; + +const initialState: GlobalStateType = { + value: 0, +}; + +export const exampleSlice = createSlice({ + initialState, + reducers: { + RESET: () => initialState, + SET_VALUE: (state, payload: number) => { + state.value = payload; + }, + INCREASE_VALUE: (state, payload: number) => { + state.value += payload; + }, + DECREASE_VALUE: (state, payload: number) => { + state.value -= payload; + }, + }, +}); + +/** + * 지역 컨텍스트에선 isGlobalSlice 옵션을 사용하지 않는것을 권장합니다. + */ +export const useExampleAccessAbleSlice = () => { + return useSlice(exampleSlice, { access: 'global' }); +}; + +export const useExampleSlice = () => { + return useSlice(exampleSlice); +}; diff --git a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/useExamplePageContext.ts b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/useExamplePageContext.ts index faff611c..328909e4 100644 --- a/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/useExamplePageContext.ts +++ b/src/components/TokDocsDevTool/components/TokDocsModal/components/ExampleSection/components/ContextSection/context/useExamplePageContext.ts @@ -1,13 +1,20 @@ import { createContextSelector } from '@/utils/react/create-context-selector'; -import { useExamplePageHandlers } from './hooks/useExamplePageHandlers'; -import { useExamplePageState } from './hooks/useExamplePageState'; +import { + useExampleAccessAbleSlice, + useExampleSlice, +} from './hooks/useExampleSlice'; const useExamplePage = () => { - const { state, dispatch } = useExamplePageState(); - const handler = useExamplePageHandlers({ state, dispatch }); + const [state, dispatch] = useExampleSlice(); + const [accessAbleState, accessAbleDispatch] = useExampleAccessAbleSlice(); - return { dispatch, state, handler }; + return { + state, + dispatch, + accessAbleState, + accessAbleDispatch, + }; }; export const { diff --git a/src/containers/Home/Home.tsx b/src/containers/Home/Home.tsx index 66ec68f7..95bacc90 100644 --- a/src/containers/Home/Home.tsx +++ b/src/containers/Home/Home.tsx @@ -1,52 +1,10 @@ -import { Box, Button, Text } from '@chakra-ui/react'; - -import { homeSlice } from '@/contexts/pages/home/hooks/useHomePageState'; -import { useHomePageContext } from '@/contexts/pages/home/useHomePageContext'; +import { Box, Text } from '@chakra-ui/react'; function Home() { - console.log('hi'); return ( HomePage - - ); } export default Home; - -const Count = () => { - const count = useHomePageContext((ctx) => ctx.state.value); - return ( - - {count} - - ); -}; - -const Updator = () => { - const dispatch = useHomePageContext((ctx) => ctx.dispatch); - - return ( - - - - - - ); -}; diff --git a/src/contexts/global/hooks/useGlobalState.ts b/src/contexts/global/hooks/useGlobalState.ts index f4018c36..5a0bb719 100644 --- a/src/contexts/global/hooks/useGlobalState.ts +++ b/src/contexts/global/hooks/useGlobalState.ts @@ -43,7 +43,7 @@ export const globalSlice = createSlice({ }); export const useGlobalState = () => { - const [state, dispatch] = useSlice(globalSlice, { isGlobalSlice: true }); + const [state, dispatch] = useSlice(globalSlice, { access: 'global' }); return { state, dispatch }; }; diff --git a/src/hooks/useSlice.ts b/src/hooks/useSlice.ts index 8fd458f6..49a85d59 100644 --- a/src/hooks/useSlice.ts +++ b/src/hooks/useSlice.ts @@ -3,11 +3,16 @@ import { useEffect, useReducer } from 'react'; import { createSlice } from '@/utils/react/create-slice'; import { ReducerMap } from '@/utils/react/types/reducer'; +/** + * @param slice createSlice로 생성한 slice 객체 + * @param options.access access 값이 global 일 경우, 외부 slice 상태 변경을 감지하여 리랜더링 할수 있습니다. 지역 컨텍스트에서는 컴포넌트의 언마운트가 될시, 상태가 초기화 되지 않을 수 있으므로 access 옵션을 사용하지 않는것을 권장합니다. + * @returns [state, dispatch] + */ export const useSlice = >( slice: ReturnType>, - options?: { isGlobalSlice: boolean }, + options?: { access?: 'local' | 'global' }, ) => { - const _slice = options?.isGlobalSlice ? slice : slice.copy(); + const _slice = options?.access === 'global' ? slice : slice.copy(); const [state, dispatch] = useReducer(_slice.reducer, _slice.getState()); useEffect(() => { diff --git a/src/utils/react/create-slice.ts b/src/utils/react/create-slice.ts index 6adfcdda..dfbb527e 100644 --- a/src/utils/react/create-slice.ts +++ b/src/utils/react/create-slice.ts @@ -1,24 +1,13 @@ import { produce } from 'immer'; -import { ActionsByMap, ReducerMap } from './types/reducer'; - -type CreateReducerParams> = { - initialState: S; - reducers: R; -}; - -type CreateSliceReturn> = { - initialState: S; - reducer: ( - state: S, - action: ActionsByMap | { type: 'RESET'; payload?: S }, - ) => S; - reducers: R; - getState: () => S; - setState: (next: S | ((prev: S) => S)) => void; - subscribe: (listener: () => void) => () => void; - copy: () => CreateSliceReturn; -}; +import { runIfFn } from '../validate/run-if-fn'; +import { + ActionWithReset, + CreateReducerParams, + CreateSliceReturn, + ReducerMap, + ReducerWithResetAction, +} from './types/reducer'; export const createSlice = >({ initialState, @@ -28,28 +17,16 @@ export const createSlice = >({ const listeners = new Set<() => void>(); - const reducer = produce( - ( - state: S, - action: ActionsByMap | { type: 'RESET'; payload?: S }, - ): S => { - if (action.type === 'RESET') return (action.payload || initialState) as S; - const reducer = reducers[action.type]; - return reducer(state, action.payload) as S; - }, - ) as ( - state: S, - action: ActionsByMap | { type: 'RESET'; payload?: S }, - ) => S; + const reducer: ReducerWithResetAction = produce((state, action) => { + if (action.type === 'RESET') return (action.payload || initialState) as S; + const reducer = reducers[action.type]; + return reducer(state as S, action.payload); + }) as ReducerWithResetAction; const getState = () => state; const setState = (next: S | ((prev: S) => S)) => { - if (typeof next === 'function') { - state = (next as Function)(state); - } else { - state = next; - } + state = runIfFn(next, state); listeners.forEach((listener) => listener()); }; @@ -60,10 +37,15 @@ export const createSlice = >({ const copy = () => createSlice({ initialState, reducers }); + const dispatch = (action: ActionWithReset) => { + setState(reducer(state, action)); + }; + return { initialState, reducers, reducer, + dispatch, getState, setState, subscribe, diff --git a/src/utils/react/types/reducer.ts b/src/utils/react/types/reducer.ts index a57f22bb..0ed440f9 100644 --- a/src/utils/react/types/reducer.ts +++ b/src/utils/react/types/reducer.ts @@ -15,3 +15,66 @@ export type ActionsByMap> = { payload: PayloadFrom; }; }[keyof R]; + +export type ActionWithReset> = + | ActionsByMap + | { type: 'RESET'; payload?: S }; + +export type ReducerWithResetAction> = ( + state: S, + action: ActionWithReset, +) => S; + +export type CreateReducerParams> = { + /** + * 초기화 데이터 + */ + initialState: S; + /** + * 이전 상태를 받아 다음상태를 리턴하는 함수들의 집합입니다. + * 해당 객체의 key 값이 dispatch 의 type 값이 됩니다. + */ + reducers: R; +}; + +export type CreateSliceReturn> = { + /** + * 초기화 데이터 + */ + initialState: S; + /** + * 이전 상태를 받아 다음상태를 리턴하는 함수들의 집합입니다. + * 해당 객체의 key 값이 dispatch 의 type 값이 됩니다. + */ + reducers: R; + /** + * reducers 객체의 키값과 각각의 payload 를 묶어 타입정의가 된 Reducer 함수 입니다. + * reset action 을 추가로 가지고 있습니다. + */ + reducer: ReducerWithResetAction; + /** + * 현재 상태를 반환합니다. + */ + getState: () => S; + /** + * 상태를 변경합니다. + * @param next 다음 상태 또는 이전 상태를 받아 다음 상태를 리턴하는 함수 + */ + setState: (next: S | ((prev: S) => S)) => void; + + /** + * action 을 dispatch 합니다. 컴포넌트 외부에서 상태를 변경할 때 주로 사용됩니다. + * @param action reset 을 포함한 action + */ + dispatch: (action: ActionWithReset) => void; + + /** + * 상태 변경을 구독합니다. + * @returns 구독 해제 함수 + */ + subscribe: (listener: () => void) => () => void; + /** + * 현재 slice 를 복사합니다. + */ + copy: () => CreateSliceReturn; +};