Shadcn Hooks

createContext

Create a context object.

isBrowser

npx shadcn@latest add "https://shadcn-hooks.vercel.app/r/create-context.json"
pnpm dlx shadcn@latest add "https://shadcn-hooks.vercel.app/r/create-context.json"
yarn dlx shadcn@latest add "https://shadcn-hooks.vercel.app/r/create-context.json"
bun x shadcn@latest add "https://shadcn-hooks.vercel.app/r/create-context.json"

Copy and paste the following code into your project.

create-context.tsx
import * as React from 'react'

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

  const Provider: React.FC<ContextValueType & { children: React.ReactNode }> = (
    props,
  ) => {
    const { children, ...context } = props
    // Only re-memoize when prop values change

    const value = React.useMemo(
      () => context,
      // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
      Object.values(context),
    ) as ContextValueType
    return <Context value={value}>{children}</Context>
  }

  Provider.displayName = `${rootComponentName}Provider`

  function useContext(consumerName: string) {
    const context = React.use(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}\``,
    )
  }

  return [Provider, useContext] as const
}

/* -------------------------------------------------------------------------------------------------
 * createContextScope
 * -----------------------------------------------------------------------------------------------*/

type Scope<C = any> = { [scopeName: string]: React.Context<C>[] } | undefined
type ScopeHook = (scope: Scope) => { [__scopeProp: string]: Scope }
interface CreateScope {
  scopeName: string
  (): ScopeHook
}

function createContextScope(
  scopeName: string,
  createContextScopeDeps: CreateScope[] = [],
) {
  let defaultContexts: any[] = []

  /* -----------------------------------------------------------------------------------------------
   * createContext
   * ---------------------------------------------------------------------------------------------*/

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

    const Provider: React.FC<
      ContextValueType & {
        scope: Scope<ContextValueType>
        children: React.ReactNode
      }
    > = (props) => {
      const { scope, children, ...context } = props
      const Context = scope?.[scopeName]?.[index] || BaseContext
      // Only re-memoize when prop values change

      const value = React.useMemo(
        () => context,
        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
        Object.values(context),
      ) as ContextValueType
      return <Context value={value}>{children}</Context>
    }

    Provider.displayName = `${rootComponentName}Provider`

    function useContext(
      consumerName: string,
      scope: Scope<ContextValueType | undefined>,
    ) {
      const Context = scope?.[scopeName]?.[index] || BaseContext
      const context = React.use(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}\``,
      )
    }

    return [Provider, useContext] as const
  }

  /* -----------------------------------------------------------------------------------------------
   * createScope
   * ---------------------------------------------------------------------------------------------*/

  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],
      )
    }
  }

  createScope.scopeName = scopeName
  return [
    createContext,
    composeContextScopes(createScope, ...createContextScopeDeps),
  ] as const
}

/* -------------------------------------------------------------------------------------------------
 * composeContextScopes
 * -----------------------------------------------------------------------------------------------*/

function composeContextScopes(
  ...scopes: [CreateScope, ...CreateScope[]]
): CreateScope {
  const baseScope = scopes[0]
  if (scopes.length === 1) return baseScope

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

    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
}

/* -----------------------------------------------------------------------------------------------*/

export { createContext, createContextScope }
export type { CreateScope, Scope }

credits

Last updated on

On this page