사툰사툰

createSafeContext

React Context API를 안전하게 생성해주는 팩토리 함수입니다

3

import * as React from 'react';

/**
 * Context Provider와 useContext 훅 쌍을 생성하는 팩토리 함수입니다.
 *
 * Provider 없이 훅을 사용하면 런타임 에러를 던져 잘못된 사용을 조기에 감지할 수 있다.
 *
 * @reference https://github.com/toss/react-simplikit/blob/main/packages/core/src/utils/buildContext/buildContext.tsx
 * @param rootComponentName - Provider의 displayName 및 에러 메시지에 사용될 루트 컴포넌트 이름
 * @param defaultContext - Context 내 기본값
 * @returns `[Provider, useContext]` 튜플
 *
 * @example
 * const [FooProvider, useFoo] = createSafeContext<FooContextValue>('Foo');
 *
 * function FooProvider({ value, children }) {
 *   return <FooProvider {...value}>{children}</FooProvider>;
 * }
 *
 * function ChildComponent() {
 *   const foo = useFoo(); // Provider 밖에서 호출하면 에러
 * }
 */
export function createSafeContext<ContextValueType extends object | null>(
  rootComponentName: string,
  defaultContext?: ContextValueType,
) {
  const Context = React.createContext<ContextValueType | undefined>(defaultContext);

  function Provider(props: React.PropsWithChildren<ContextValueType>) {
    const { children, ...context } = props;
    // context 객체의 참조 안정성을 위해 values 기반으로 메모이제이션
    const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;
    return <Context.Provider value={value}>{children}</Context.Provider>;
  }

  function useContext(consumerName?: string) {
    const context = React.useContext(Context);
    if (context) return context;
    // defaultContext가 명시적으로 제공된 경우 fallback으로 반환
    if (defaultContext !== undefined) return defaultContext;
    throw new Error(
      `\`${consumerName || rootComponentName}\`은 반드시 ${rootComponentName}Provider 안에서 사용해야 합니다`,
    );
  }

  // React DevTools에서 컴포넌트를 식별할 수 있도록 displayName 설정
  Provider.displayName = rootComponentName + 'Provider';
  return [Provider, useContext] as const;
}