Skip to content

Instantly share code, notes, and snippets.

@jose-mdz
Created January 13, 2025 02:41
Show Gist options
  • Save jose-mdz/a429bc0c40a52aca953f44ab3dee3192 to your computer and use it in GitHub Desktop.
Save jose-mdz/a429bc0c40a52aca953f44ab3dee3192 to your computer and use it in GitHub Desktop.

providerFactory — A React Provider Factory

This Gist provides a utility function that simplifies creating a React Context and Provider based on a custom hook. Unlike typical approaches, this factory allows you to optionally pass props to your custom hook inside the Provider component, offering more flexibility for dynamic context values.


Motivation

In many React applications, you create a custom hook for certain stateful logic and then wrap it inside a Context Provider to make that state accessible throughout your component tree. However, it’s not always straightforward if your hook depends on certain parameters (e.g., a userId), because a regular factory might not allow for passing dynamic props to the hook.

This providerFactory solves that issue by enabling:

  1. Forwarding props from the Provider to your custom hook (if needed).
  2. Returning a typed consumer hook (useProvider) that safely retrieves the context value.

Simple Usage (Hook Without Arguments)

1. Create a Simple Hook and Its Provider

If you have a hook that doesn’t need any external props, you can define it and immediately create your Provider/Consumer pair using providerFactory:

// userFormContext.ts

import { useState } from "react";
import { providerFactory } from "./provider-factory";

function useUserForm() {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");

  return {
    username,
    email,
    setUsername,
    setEmail,
  };
}

export const [UserFormProvider, useUserFormContext] = providerFactory(useUserForm);

2. Wrap Your Component Tree

// App.tsx

import React from "react";
import { UserFormProvider } from "./userFormContext";
import { UserForm } from "./UserForm";

export function App() {
  return (
    <UserFormProvider>
      <UserForm />
    </UserFormProvider>
  );
}

3. Consume the Context

// UserForm.tsx

import React from "react";
import { useUserFormContext } from "./userFormContext";

export function UserForm() {
  const { username, email, setUsername, setEmail } = useUserFormContext();

  return (
    <form>
      <label>
        Username
        <input
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
      </label>
      <label>
        Email
        <input
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
    </form>
  );
}

Using Arguments (Hook With Props)

1. Create a Hook That Accepts Props and Its Provider

Below is an example of a custom hook that accepts an object with a userId property, combined with the call to providerFactory:

// userContext.ts

import { useState, useEffect } from "react";
import { providerFactory } from "./provider-factory";

function useUserState({ userId }: { userId: number }) {
  const [user, setUser] = useState<{ id: number; name: string } | null>(null);

  useEffect(() => {
    // Example: fetch user details
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, [userId]);

  return {
    user,
    setUser,
  };
}

// Create the provider and consumer hook in one go
export const [UserProvider, useUser] = providerFactory(useUserState);

2. Wrap Your Component Tree (Passing Props)

You can now use <UserProvider> wherever you’d like in your component tree, passing any necessary props (userId in this example) to the Provider:

// App.tsx

import React from "react";
import { UserProvider } from "./userContext";
import { UserProfile } from "./UserProfile";

export function App() {
  return (
    <UserProvider userId={123}>
      <UserProfile />
    </UserProvider>
  );
}

3. Consume the Context Value

Inside any descendant of <UserProvider>, call useUser() to get the hook’s return value:

// UserProfile.tsx

import React from "react";
import { useUser } from "./userContext";

export function UserProfile() {
  const { user } = useUser();

  if (!user) return <div>Loading...</div>;
  return <div>{`User: ${user.name}`}</div>;
}

Notes

  • The generic signature export function providerFactory<T, Props extends object = {}>(hook: (props: Props) => T) means this factory can be reused for any hook that returns any type T and accepts any props Props.
  • If your hook requires no props, you can simply provide an empty object type or skip specifying it entirely.
  • This pattern can be scaled to multiple providers for different contexts in your app, all while maintaining strong type-safety.

Contributing

Feel free to fork this Gist and modify it to your needs, or open an issue if you spot a problem or have a suggestion.


License

This code is provided under the MIT license. Please feel free to use it in your own projects!

// provider-factory.tsx
import type React from "react";
import { createContext, useContext, type PropsWithChildren } from "react";
export function providerFactory<T, Props extends Record<string, unknown> = Record<string, unknown>>(
hook: (props: Props) => T,
): [React.FC<PropsWithChildren<Props>>, () => T] {
const Context = createContext<T | undefined>(undefined);
const Provider: React.FC<PropsWithChildren<Props>> = ({ children, ...props }) => {
const state = hook(props as Props);
return <Context.Provider value={state}>{children}</Context.Provider>;
};
const useProvider = () => {
const value = useContext(Context);
if (value === undefined) {
throw new Error("Trying to use context outside a valid provider");
}
return value;
};
return [Provider, useProvider];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment