[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 |