Skip to content

Instantly share code, notes, and snippets.

@artalar
Last active August 15, 2025 15:51
Show Gist options
  • Save artalar/0b08731db409dec7fe1aac8003ed595e to your computer and use it in GitHub Desktop.
Save artalar/0b08731db409dec7fe1aac8003ed595e to your computer and use it in GitHub Desktop.
import React from 'react';
import { atom } from '@reatom/framework';
import { useAtom, useCtx } from '@reatom/npm-react';
type CreateContexts = {
<T extends Record<string, () => Record<string, unknown>>>(
contextsHook: T,
): {
// @ts-expect-error bad type inference
[K in keyof T as `use${K}Context`]: {
(): ReturnType<T[K]>;
<Prop extends keyof ReturnType<T[K]>>(prop: Prop): ReturnType<T[K]>[Prop];
<Selected>(selector: (state: ReturnType<T[K]>) => Selected): Selected;
Provider: React.FC<React.PropsWithChildren>;
};
};
};
const NEVER_VALUE = {};
// @ts-expect-error generic mismatch
export const createContexts: CreateContexts = (contextsHook) => {
const result: Record<string, () => unknown> = {};
for (const [name, useContextLogic] of Object.entries(contextsHook)) {
const hookName = `use${name}Context`;
const providerName = `${name}Provider`;
const context = React.createContext(NEVER_VALUE as ReturnType<typeof useContextLogic>);
const contextAtom = atom(NEVER_VALUE as ReturnType<typeof useContextLogic>, `${name}Atom`);
const Provider = ({ children }: React.PropsWithChildren): React.JSX.Element => {
const state = useContextLogic();
const value = React.useMemo(
() => state,
// eslint-disable-next-line react-hooks/exhaustive-deps
Object.values(state),
);
const ctx = useCtx();
if (ctx.get(contextAtom) !== value) {
contextAtom(ctx, value);
}
return React.createElement(context.Provider, {
value,
children,
});
};
Provider.displayName = providerName;
result[hookName] = Object.assign(
(selector?: string | ((state: ReturnType<typeof useContextLogic>) => unknown)) => {
if (selector) {
return useAtom(
(ctx) => {
const state = ctx.spy(contextAtom);
return typeof selector === 'function' ? selector(state) : state[selector];
},
[],
{ name: `${name}.useAtom._selector` },
)[0];
}
const value = React.useContext(context);
if (!value || value === NEVER_VALUE) {
throw new Error(`${hookName} must be used within a ${providerName}`);
}
return value;
},
{
Provider,
},
);
}
return result;
};
import React from 'react';
const NEVER_VALUE = {};
/**
* Creates React context hooks with providers for multiple contexts at once.
*
* This utility generates typed context hooks and providers from a configuration object,
* eliminating boilerplate code for creating multiple related contexts.
*
* @template Name - The string literal type for context names (used to generate hook names)
* @template Return - The type of value returned by the context hook
* @template Params - The type of parameters required by the context logic (defaults to void)
*
* @param contextsHook - An object where keys are context names and values are functions
* that define the context logic. Each function receives params and
* returns the context value.
*
* @returns An object containing generated context hooks. Each hook has:
* - A function that returns the context value (throws if used outside provider)
* - A Provider component that wraps children with the context
*
* @example
* ```tsx
* // Create contexts without parameters
* const { useUserContext, useThemeContext } = createContexts({
* User() {
* const [user, setUser] = useState(null);
* return { user, setUser };
* },
* Theme() {
* const [theme, setTheme] = useState('light');
* return { theme, setTheme };
* },
* });
*
* // Usage in component
* function App() {
* return (
* <useUserContext.Provider>
* <useThemeContext.Provider>
* <MyComponent />
* </useThemeContext.Provider>
* </useUserContext.Provider>
* );
* }
*
* function MyComponent() {
* const { user, setUser } = useUserContext();
* const { theme, setTheme } = useThemeContext();
* // ...
* }
* ```
*
* @example
* ```tsx
* // Create contexts with parameters
* const { useApiContext } = createContexts({
* Api(baseUrl: string) {
* const client = useMemo(() => new ApiClient(baseUrl), [baseUrl]);
* return { client };
* },
* });
*
* // Usage with parameters
* function App() {
* return (
* <useApiContext.Provider params="https://api.example.com">
* <MyComponent />
* </useApiContext.Provider>
* );
* }
*
* function MyComponent() {
* const { client } = useApiContext();
* // ...
* }
* ```
*
* @throws {Error} When a context hook is called outside its corresponding Provider
*/
export const createContexts = <Name extends string, Return, Params = void>(
contextsHook: Record<Name, (params: Params) => Return>,
): Record<
`use${Name}Context`,
{
(params: Params): Return;
Provider: React.FC<Params extends void ? React.PropsWithChildren : React.PropsWithChildren<{ params: Params }>>;
}
> => {
const result: Record<string, unknown> = {};
for (const [name, useContextLogic] of Object.entries(contextsHook)) {
const hookName = `use${name}Context`;
const providerName = `${name}Provider`;
const context = React.createContext(NEVER_VALUE as Return);
function Provider({ params, children }: React.PropsWithChildren<Record<string, unknown>>): JSX.Element {
const state = (useContextLogic as (params: Params) => Return)(params as Params);
const value = React.useMemo(
() => state,
// eslint-disable-next-line react-hooks/exhaustive-deps
Object.values(state as Record<string, unknown>),
);
return React.createElement(context.Provider, { value }, children);
}
Provider.displayName = providerName;
result[hookName] = Object.assign(
() => {
const value = React.useContext(context);
if (!value || value === NEVER_VALUE) {
throw new Error(`${hookName} must be used within a ${providerName}`);
}
return value;
},
{
Provider,
},
);
}
return result as Record<
`use${Name}Context`,
{
(params: Params): Return;
Provider: React.FC<Params extends void ? React.PropsWithChildren : React.PropsWithChildren<{ params: Params }>>;
}
>;
};
// create
export const { useAuth } = createContexts({
Auth() {
const [a, setA] = React.useState("a");
const [b, setB] = React.useState("b");
return {
a,
b,
setA,
setB,
};
},
});
// setup
<useAuth.Provider>{children}</useAuth.Provider>;
// use
const { a, b, setA, setB } = useAuth();
// select
const setA = useAuth("setA");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment