사용법1. context를 생성한다.2. provider를 만든다. (값 선언, 초기값 설정, 상태 변경 함수 제공)3. 상태값 조회 훅 등 상태와 관련된 훅들 생성팁createContext 시에는 디폴트 상태값을 넣지 않는 것이 좋다두 가지만 export { use상태Provider, use상태 }Reducer를 안쓰는 좀 더 간단한 예시HOC 패턴을 활용하자 (콘텍스트 결합! 클린코드)
- 효율적인 사용법(useReducer 활용) : https://goongoguma.github.io/2021/06/05/How-to-use-React-Context-effectively/
- useState 활용 : https://levelup.gitconnected.com/react-context-the-definitive-guide-to-best-practices-fd095568be03
사용법
1. context를 생성한다.
팁에서 다시 다루지만, 컨텍스트 생성 시점에는 기본값을 할당하지 않는 것이 좋다.
import { createContext } from 'react'; type State = {}; const WakeUpCallContext = createContext<State | undefined>(undefined);
2. provider를 만든다. (값 선언, 초기값 설정, 상태 변경 함수 제공)
상태와 상태변경 함수 등 상태와 관련된 로직들을 선언하고, 컨텍스트에 제공하는 역할을 한다.
provider
를 거침으로써 초기값들이 설정되고, 비로소 useGet@@
등dml 함수들을 통해 상태를 조회하고 사용하게 된다.ex) 특정 상황에서 호출되길 원하는 함수들을 등록하는 컨텍스를 만들고 싶다.
export const WakeUpCallProvider: FC = ({ children }) => { // contextAPI 에서 다루고자 하는 상태 선언 // const functions = useRef<(() => void)[]>([]); return ( <WakeUpCallContext.Provider value={ref}> {children} </WakeUpCallContext.Provider> ); };
3. 상태값 조회 훅 등 상태와 관련된 훅들 생성
ex) 특정 상황에서 호출되길 원하는 함수들을 등록하는 컨텍스를 만들고 싶다.
따라서 함수를 등록하는 훅, 등록된 함수들을 조회할 수 있는 훅 등을 만들 수 있다.
export const useGetWakeUpCallFunctions = (): State['current'] => { const context = useContext(WakeUpCallContext); if (context === undefined) { throw new Error( 'useGetWakeUpCallFunctions must be used within a WakeUpCallProvider', ); } return context.current; }; export const useAddAliveStateListener = (listener: VoidFunctionType): void => { const wakeUpCallFunctions = useGetWakeUpCallFunctions(); useEffect(() => { wakeUpCallFunctions.push(listener); return (): void => { const index = wakeUpCallFunctions.indexOf(listener); if (index > -1) { wakeUpCallFunctions.splice(index, 1); } }; }, [listener, wakeUpCallFunctions]); };
팁
createContext
시에는 디폴트 상태값을 넣지 않는 것이 좋다
createContext
시에도 기본값을 설정할 수 있는데, 웬만한 상황에서는 하지 않는 것을 추천한다.Provider
로 감싸는 과정에서도 value
를 설정할 수 있다.context만 만들어놓고,
provider
로 감싸지 않고 코드를 작성하는 오류를 범할 시 예외를 발생시킬 수 있도록 createContext
시에는 초기 상태값을 설정하지 않고 undefined
또는 null
로 두는 것이 좋다.function useCount() { const context = React.useContext(CountStateContext) if (context === undefined) { throw new Error('useCount must be used within a CountProvider') } return context; }
두 가지만 export { use상태Provider
, use상태
}
context에서 다룰 상태는
{상태, 상태변경함수, 등등}
을 담고, 이 모든 것을 use상태
훅을 통해 얻을 수 있도록 한다.상태를 사용하고자 하는 곳에서 감싸주기 위한
Provider
와, 실제로 상태를 사용할 수 있는 use상태
훅 두가지만 export
하도록 만드는 것이 사용성에 좋다.// src/count-context.tsx import * as React from 'react' type Action = {type: 'increment'} | {type: 'decrement'} type Dispatch = (action: Action) => void type State = {count: number} type CountProviderProps = {children: React.ReactNode} const CountStateContext = React.createContext< {state: State; dispatch: Dispatch} | undefined >(undefined) function countReducer(state: State, action: Action) { switch (action.type) { case 'increment': { return {count: state.count + 1} } default: { throw new Error(`Unhandled action type: ${action.type}`) } } } function CountProvider({children}: CountProviderProps) { const [state, dispatch] = React.useReducer(countReducer, {count: 0}) // NOTE: you *might* need to memoize this value // Learn more in http://kcd.im/optimize-context const value = {state, dispatch} return ( <CountStateContext.Provider value={value}> {children} </CountStateContext.Provider> ) } function useCount() { const context = React.useContext(CountStateContext) if (context === undefined) { throw new Error('useCount must be used within a CountProvider') } return context } export {CountProvider, useCount}
Reducer를 안쓰는 좀 더 간단한 예시
import React, { createContext, type Dispatch, type FC, type SetStateAction, useState, } from 'react'; type State = { isOnDispensing: boolean; setIsOnDispensing: Dispatch<SetStateAction<boolean>>; }; const IsOnDispensingContext = createContext<State | undefined>(undefined); const IsOnDispensingProvider: FC = ({ children }) => { const [isOnDispensing, setIsOnDispensing] = useState<boolean>(false); return ( <IsOnDispensingContext.Provider value={{ isOnDispensing, setIsOnDispensing }} > {children} </IsOnDispensingContext.Provider> ); }; const useIsOnDispensing = (): State => { const context = React.useContext(IsOnDispensingContext); if (context === undefined) { throw new Error( 'useIsOnDispensing must be used within a IsOnDispensingProvider', ); } return context; }; export { IsOnDispensingProvider, useIsOnDispensing };
HOC 패턴을 활용하자 (콘텍스트 결합! 클린코드)
const hasProperty = (object, key) => (object ? Object.hasOwnProperty.call(object, key) : false) const hasProps = (arg) => hasProperty(arg, 'provider') && hasProperty(arg, 'props') export const withContextProviders = (...providers) => (Component) => (props) => providers.reduceRight((acc, prov) => { let Provider = prov if (hasProps(prov)) { Provider = prov.context const providerProps = prov.props return <Provider {...providerProps}>{acc}</Provider> } return <Provider>{acc}</Provider> }, <Component {...props} />)
const providers = [ { provider: ThemeProvider, props: { darkMode: true } }, { provider: I8nProvider, props: { lang: 'en' } }, ] const App = () => { return ( <Layout> <Main /> </Layout> ) } export default withContextProviders(...providers)(App)
import React, { type ComponentProps, type FC } from 'react'; export const combineProviders = (providers: FC[]): FC => { return providers.reduce( (Combined, CurrentProvider) => { return ({ children }: ComponentProps<FC>): JSX.Element => { return ( <Combined> <CurrentProvider>{children}</CurrentProvider> </Combined> ); }; }, ({ children }) => <>{children}</>, ); };
const SimpleAuthProviders = combineProviders([ OrganizationProvider, SimpleAuthWayProvider, NextQuestionnaireProvider, ]); export const HealthInsuranceNavigator: VFC = () => { return ( <SimpleAuthProviders> <CustomSafeAreaView> ... </CustomSafeAreaView> </SimpleAuthProviders> ); }