본문 바로가기
오픈소스/ui 라이브러리

@radix-ui / createContext 훑어보기

by 위그든씨 2024. 3. 26.

@radix-ui/ createContext

[primitives/packages/react/context/src/createContext.tsx at main · radix-ui/primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos. - radix-ui/primitives

github.com](https://github.com/radix-ui/primitives/blob/main/packages/react/context/src/createContext.tsx)

1. createContext

// Compound Component pattern을 위한 React.createContext 활용 (컨텍스트 생성 후 provider를 통해 value 공동 사용)

function createContext<ContextValueType extends object | null>(
  rootComponentName: string,
  defaultContext?: ContextValueType
) {
  const Context = React.createContext<ContextValueType | undefined>(defaultContext);

// 1.
  function Provider(props: ContextValueType & { children: React.ReactNode }) {
    const { children, ...context } = props;
    // Only re-memoize when prop values change
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;
    return <Context.Provider value={value}>{children}</Context.Provider>;
  }
 //2.
  function useContext(consumerName: string) {
    const context = React.useContext(Context);
    if (context) return context;
    if (defaultContext !== undefined) return defaultContext;
    // if a defaultContext wasn't specified, it's a required context.
    throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
  }

  Provider.displayName = rootComponentName + 'Provider';
  return [Provider, useContext] as const;
}
function Provider(props: ContextValueType & { children: React.ReactNode }) {
    const { children, ...context } = props;
    // Only re-memoize when prop values change
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;
    return <Context.Provider value={value}>{children}</Context.Provider>;
  }

  //useMemo() : 주어진 함수의 결과값을 메모이제이션(캐싱)하여, 
  //            동일한 입력값에 대해서는 함수를 다시 실행하지 않고 이전에 계산된 값을 재사용하는 기능
  //            context 객체의 값들을 의존성 배열로 사용
  //     이 패턴은 특히 React의 컨텍스트를 사용하여 글로벌 상태를 관리할 때 유용
function useContext(consumerName: string) {
    const context = React.useContext(Context);
    if (context) return context;
    if (defaultContext !== undefined) return defaultContext;
    // if a defaultContext wasn't specified, it's a required context.
    throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
  }

  // useContext를 통해 위에서 생성한 컨텍스트의 value들을 반환
  // 유효하지 않은 값이면 에러 처리
Provider.displayName = rootComponentName + 'Provider'; // 개발자모드에서 디버깅 목적
return [Provider, useContext] as const;    
//as const를 사용하면 TypeScript는 반환되는 배열을 튜플(tuple)로 인식하게 되어, 배열의 각 요소의 타입과 순서가 정확하게 유지

2. createContextScope

  • React 컴포넌트들 사이에서 공유할 수 있는 컨텍스트 스코프를 생성하는 고급 유틸리티
  • 이 함수는 특히 대규모 애플리케이션 또는 라이브러리에서 여러 컨텍스트를 관리할 때 유용
  • 각 컨텍스트는 특정 범위 내에서 데이터를 공유하고, 동일한 스코프 내의 컴포넌트들은 이 데이터에 접근 =>여러 컨텍스트를 하나의 스코프로 묶고 관리할 수 있는 기능을 제공
  • scopeName: string, createContextScopeDeps: CreateScope[] = [] 을 인자로 받음
  • createContext , createScope 함수 정의 후
  • [createContext, composeContextScopes(createScope, ...createContextScopeDeps)] as const; 을 리턴
  • createContext: 컨텍스트 생성 . 그 컨텍스트를 스코프에 속하게 만들기 위해 defaultContexts 배열에 해당 컨텍스트의 기본값을 추가
  • createScope:생성된 모든 컨텍스트에 대한 기본 컨텍스트 객체를 생성. 해당 함수는 useScope 훅도 같이 반환하는데 이를 통해 스코프에 포함된 모든 컨텍스트를 제공하고, 각 컨텍스트의 현재 값을 포함하는 객체를 반환
// eg. accordion중 일부 (createCollection 훑어오기 )

const [Collection, useCollection, createCollectionScope] =
  createCollection<AccordionTriggerElement>(ACCORDION_NAME);

type ScopedProps<P> = P & { __scopeAccordion?: Scope };
const [createAccordionContext, createAccordionScope] = createContextScope(ACCORDION_NAME, [
  createCollectionScope,
  createCollapsibleScope,
]);
type Scope<C = any> = { [scopeName: string]: React.Context<C>[] } | undefined;
type ScopeHook = (scope: Scope) => { [__scopeProp: string]: Scope };

interface CreateScope {
  scopeName: string;
  (): ScopeHook;
}
// createContextScope가 반환하는 createContext

let defaultContexts: any[] = [];

function createContext<ContextValueType extends object | null>(
    rootComponentName: string,
    defaultContext?: ContextValueType
  ) {
    const BaseContext = React.createContext<ContextValueType | undefined>(defaultContext);
    const index = defaultContexts.length;
    defaultContexts = [...defaultContexts, defaultContext];

    function Provider(
      props: ContextValueType & { scope: Scope<ContextValueType>; children: React.ReactNode }
    ) {
      const { scope, children, ...context } = props;
      const Context = scope?.[scopeName][index] || BaseContext;
      // Only re-memoize when prop values change
      // eslint-disable-next-line react-hooks/exhaustive-deps
      const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;
      return <Context.Provider value={value}>{children}</Context.Provider>;
    }


    function useContext(consumerName: string, scope: Scope<ContextValueType | undefined>) {
      const Context = scope?.[scopeName][index] || BaseContext;
      const context = React.useContext(Context);
      if (context) return context;
      if (defaultContext !== undefined) return defaultContext;
      // if a defaultContext wasn't specified, it's a required context.
      throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
    }

    Provider.displayName = rootComponentName + 'Provider';
    return [Provider, useContext] as const;
  }
const createScope: CreateScope = () => {
    const scopeContexts = defaultContexts.map((defaultContext) => {
      return React.createContext(defaultContext);
    });
    return function useScope(scope: Scope) {
      const contexts = scope?.[scopeName] || scopeContexts;
      return React.useMemo(
        () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
        [scope, contexts]
      );
    };
  };


// 묶여진 컨텍스트끼리의 스코프 생성

 

3. composeContextScopes

  • 여러 컨텍스트 스코프를 하나의 복합 스코프로 결합하는 역할
function composeContextScopes(...scopes: CreateScope[]) {
  const baseScope = scopes[0];
  if (scopes.length === 1) return baseScope;

  const createScope: CreateScope = () => {
    const scopeHooks = scopes.map((createScope) => ({
      useScope: createScope(),
      scopeName: createScope.scopeName,
    }));

	//인수로 받은 overrideScopes를 기반으로 각 스코프의 상태를 계산하고, 모든 스코프의 상태를 하나의 객체로 병합
    return function useComposedScopes(overrideScopes) {
      const nextScopes = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {
        // We are calling a hook inside a callback which React warns against to avoid inconsistent
        // renders, however, scoping doesn't have render side effects so we ignore the rule.
        // eslint-disable-next-line react-hooks/rules-of-hooks
        const scopeProps = useScope(overrideScopes);
        const currentScope = scopeProps[`__scope${scopeName}`];
        return { ...nextScopes, ...currentScope };
      }, {});

      return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
    };
  };

  createScope.scopeName = baseScope.scopeName;
  return createScope;
}