-
@radix-ui / createContext 훑어보기오픈소스/ui 라이브러리 2024. 3. 26. 16:50
[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; }
'오픈소스 > ui 라이브러리' 카테고리의 다른 글
shadcn/ul - Select 컴포넌트의 placeholder 색상 변경 (0) 2024.09.07 1. Compound Component Pattern (0) 2024.03.25