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.
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:
- Forwarding props from the Provider to your custom hook (if needed).
- Returning a typed consumer hook (
useProvider
) that safely retrieves the context value.
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);
// App.tsx
import React from "react";
import { UserFormProvider } from "./userFormContext";
import { UserForm } from "./UserForm";
export function App() {
return (
<UserFormProvider>
<UserForm />
</UserFormProvider>
);
}
// 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>
);
}
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);
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>
);
}
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>;
}
- 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 typeT
and accepts any propsProps
. - 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.
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.
This code is provided under the MIT license. Please feel free to use it in your own projects!