ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @radix-ui / createContext 훑어보기
    오픈소스/ui 라이브러리 2024. 3. 26. 16:50

    @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;
    }

     

Designed by Tistory.