react-easy-provider
v0.2.1
Published
A utility to create React Provider without Context boilerplate
Readme
react-easy-provider
A utility to create React Provider without Context boilerplate
The problem
Suppose you want to create a reusable counter to avoid prop drilling, you end up going through several mechanical steps.
Step 1 — Define the value shape
type CounterContextValue = {
count: number
inc: () => void
}Step 2 — Create the Context instance
const CounterContext = createContext<CounterContextValue | undefined>(undefined)Step 3 — Build the Provider
export const CounterProvider = ({ children }: { children: ReactNode }) => {
const [count, setCount] = useState(0)
return (
<CounterContext.Provider
value={{ count, inc: () => setCount((c) => c + 1) }}
>
{children}
</CounterContext.Provider>
)
}Step 4 — Expose a consumer hook (with a optional runtime guard)
export const useCounter = () => {
const ctx = useContext(CounterContext)
if (!ctx) throw new Error("useCounter must be used within CounterProvider")
return ctx
}A few things become noticeable:
- To keep things maintainable, you usually end up splitting this into multiple files: types, Context, the Provider, and the consumer hook.
- When the logic grows, you have to update the types along with it.
- Context often feels redundant — you declare it once and rarely touch it again.
Repeating this clunky pattern enough times led to this package.
Getting started
Installation
npm install react-easy-provider
yarn add react-easy-provider
pnpm add react-easy-providerCreating a Provider
Use createProvider by defining your shared logic once. This function becomes the single source of truth for both the logic and its types — without manually creating or consuming React Context.
import { useState } from "react"
import { createProvider } from "react-easy-provider"
export const [useCounter, CounterProvider] = createProvider(
// Function that defines the shared logic and returns the context value
(initial?: number) => {
const [count, setCount] = useState(initial ?? 0)
return {
count,
inc: () => setCount((c) => c + 1),
}
},
// Optional display name for the Provider, useful for debugging
"CounterProvider"
)Now you have both the hook and the Provider — just wire them up like usual.
// Wrap your app (or part of it)
<CounterProvider defaultValue={10}>
<App />
</CounterProvider>
// Use the shared logic anywhere below the Provider
const { count, inc } = useCounter();
return (
<button onClick={inc}>
Count: {count}
</button>
);Error when the hook is used outside a Provider
Using the example useCounter hook without a corresponding Provider above it in the component tree:
const Counter = () => {
const { count } = useCounter();
return <div>{count}</div>;
};will result in the following error being thrown by default:
Error: useContextValue must be used within CounterProviderThis behavior is intentional to help catch misconfiguration early. The error message includes the Provider name to make debugging easier, so giving your Provider a clear and explicit name is strongly recommended. A well-named Provider makes it immediately obvious which wrapper is missing and where the hook is expected to be used, especially in apps with multiple Providers.
Fail quietly
You can allow to fail quietly when a Provider might not always be present, or when you want a safe default.
const { count, inc } = useCounter({
shouldFailQuietly: true,
fallback: {
count: 0,
inc: () => {},
},
});Standalone (isolated) mode
There are cases where you want to reuse the shared logic directly without setting up a Provider, and without duplicating the hook just to support both patterns. For these scenarios, you can enable standalone mode.
When standalone mode is enabled, the hook runs fully independently, ignoring any Provider even if one exists in the component tree. This allows you to reuse the same logic without creating or wrapping a Provider, which is especially useful for isolated usage such as tests or component previews.
const { count, inc } = useCounter({
standalone: true,
defaultValue: 5,
});I still want direct access to Context
Fine! createProvider returns the underlying Context as the third value.
const [useCounter, CounterProvider, CounterContext] = createProvider(
useCounterLogic,
"CounterProvider"
);API
createProvider<T, U>(fn, displayName?)
const [useValue, Provider, Context] = createProvider(fn, displayName);fn: (defaultValue?: U) => TDefines the shared logic. The return typeTbecomes the Context value type.displayName?: stringOptional Provider display name.
Returns:
useValue: (options?) => TProvider: React.FC<{ defaultValue?: U }>Context: React.Context<T | undefined>
useValue(options?)
const value = useValue(options);type Options<T, U> = {
standalone?: boolean;
defaultValue?: U;
shouldFailQuietly?: boolean;
fallback?: T;
};Inside a Provider → returns Context value
Outside a Provider:
- throws by default
- returns
fallbackifshouldFailQuietly - runs
fn(defaultValue)ifstandalone
Type inference
All types are inferred from fn.
No manual Context value types required.
