https://github.com/bonniss/react-easy-provider
React Provider without Context boilerplate
https://github.com/bonniss/react-easy-provider
react react-context react-provider react-utils
Last synced: 5 months ago
JSON representation
React Provider without Context boilerplate
- Host: GitHub
- URL: https://github.com/bonniss/react-easy-provider
- Owner: bonniss
- License: mit
- Created: 2025-12-18T17:18:03.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-12-20T03:34:06.000Z (6 months ago)
- Last Synced: 2025-12-22T12:17:32.682Z (6 months ago)
- Topics: react, react-context, react-provider, react-utils
- Language: TypeScript
- Homepage:
- Size: 16.6 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
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
```tsx
type CounterContextValue = {
count: number
inc: () => void
}
```
**Step 2** — Create the Context instance
```tsx
const CounterContext = createContext(undefined)
```
**Step 3** — Build the Provider
```tsx
export const CounterProvider = ({ children }: { children: ReactNode }) => {
const [count, setCount] = useState(0)
return (
setCount((c) => c + 1) }}
>
{children}
)
}
```
**Step 4** — Expose a consumer hook (with a optional runtime guard)
```tsx
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
```sh
npm install react-easy-provider
yarn add react-easy-provider
pnpm add react-easy-provider
```
### Creating 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**.
```tsx
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.
```tsx
// Wrap your app (or part of it)
// Use the shared logic anywhere below the Provider
const { count, inc } = useCounter();
return (
Count: {count}
);
```
### Error when the hook is used outside a Provider
Using the example `useCounter` hook without a corresponding Provider above it in the component tree:
```tsx
const Counter = () => {
const { count } = useCounter();
return
{count};
};
```
will result in the following error being thrown by default:
```html
Error: useContextValue must be used within CounterProvider
```
This 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.
```tsx
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.
```tsx
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.
```tsx
const [useCounter, CounterProvider, CounterContext] = createProvider(
useCounterLogic,
"CounterProvider"
);
```
## API
### `createProvider(fn, displayName?)`
```ts
const [useValue, Provider, Context] = createProvider(fn, displayName);
```
* **`fn: (defaultValue?: U) => T`**
Defines the shared logic.
The return type `T` becomes the Context value type.
* **`displayName?: string`**
Optional Provider display name.
**Returns:**
* **`useValue: (options?) => T`**
* **`Provider: React.FC<{ defaultValue?: U }>`**
* **`Context: React.Context`**
### `useValue(options?)`
```ts
const value = useValue(options);
```
```ts
type Options = {
standalone?: boolean;
defaultValue?: U;
shouldFailQuietly?: boolean;
fallback?: T;
};
```
* Inside a Provider → returns Context value
* Outside a Provider:
* throws by default
* returns `fallback` if `shouldFailQuietly`
* runs `fn(defaultValue)` if `standalone`
### Type inference
All types are inferred from `fn`.
No manual Context value types required.
## License
[MIT](LICENSE)