react-container-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/dist/index.d.mts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# react-container-kit
|
|
2
|
+
|
|
3
|
+
TypeScript utilities for the React container pattern — named containers, provider composition, and type helpers. Built on top of [`unstated-next`](https://github.com/jamiebuilds/unstated-next).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add react-container-kit unstated-next react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### `createContainer` (re-export)
|
|
14
|
+
|
|
15
|
+
Re-exported directly from `unstated-next`. Create a container from a custom hook:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createContainer } from 'react-container-kit';
|
|
19
|
+
|
|
20
|
+
interface Profile { firstName: string; lastName: string; }
|
|
21
|
+
|
|
22
|
+
function useProfileInternal(initialState?: Profile) {
|
|
23
|
+
const [profile, setProfile] = useState<Profile>(initialState ?? { firstName: '', lastName: '' });
|
|
24
|
+
return { profile, setProfile };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const container = createContainer(useProfileInternal);
|
|
28
|
+
export const ProfileProvider = container.Provider;
|
|
29
|
+
export const useProfile = container.useContainer;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `createNamedContainer`
|
|
33
|
+
|
|
34
|
+
Like `createContainer`, but sets a `displayName` on the Provider for better React DevTools readability. Without this, every container's provider shows as `"Provider"` in the component tree.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createNamedContainer } from 'react-container-kit';
|
|
38
|
+
|
|
39
|
+
const { Provider: ProfileProvider, useContainer: useProfile } =
|
|
40
|
+
createNamedContainer('Profile', useProfileInternal);
|
|
41
|
+
// ProfileProvider now shows as "ProfileProvider" in React DevTools
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `composeProviders`
|
|
45
|
+
|
|
46
|
+
Composes multiple provider components into a single wrapper, eliminating deeply-nested JSX. Providers are applied outermost-first (left to right).
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { composeProviders } from 'react-container-kit';
|
|
50
|
+
|
|
51
|
+
// Before — deeply nested:
|
|
52
|
+
// <SnackbarProvider>
|
|
53
|
+
// <ThemeProvider>
|
|
54
|
+
// <RouterProvider>
|
|
55
|
+
// {children}
|
|
56
|
+
// </RouterProvider>
|
|
57
|
+
// </ThemeProvider>
|
|
58
|
+
// </SnackbarProvider>
|
|
59
|
+
|
|
60
|
+
const AppProviders = composeProviders(SnackbarProvider, ThemeProvider, RouterProvider);
|
|
61
|
+
|
|
62
|
+
function App() {
|
|
63
|
+
return <AppProviders><Routes /></AppProviders>;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **Note:** `composeProviders` is suited for providers that don't require dynamic props at render time. For providers that need `initialState` computed from fetched data, continue to use JSX directly.
|
|
68
|
+
|
|
69
|
+
### Type utilities
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import type { ContainerValue, ContainerState, TypedContainer } from 'react-container-kit';
|
|
73
|
+
|
|
74
|
+
const ProfileContainer = createContainer(useProfileInternal);
|
|
75
|
+
|
|
76
|
+
// Extract the hook's return type
|
|
77
|
+
type ProfileValue = ContainerValue<typeof ProfileContainer>;
|
|
78
|
+
// -> { profile: Profile; setProfile: Dispatch<SetStateAction<Profile>> }
|
|
79
|
+
|
|
80
|
+
// Extract the initialState type
|
|
81
|
+
type ProfileInitialState = ContainerState<typeof ProfileContainer>;
|
|
82
|
+
// -> Profile
|
|
83
|
+
|
|
84
|
+
// Type a variable that holds any container
|
|
85
|
+
function logContainer(c: TypedContainer<unknown>) { ... }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Patterns from the field
|
|
89
|
+
|
|
90
|
+
Based on real-world usage, the recommended pattern for a container module is:
|
|
91
|
+
|
|
92
|
+
<!-- markdownlint-disable-next-line MD033 -->
|
|
93
|
+
<details>
|
|
94
|
+
<!-- markdownlint-disable-next-line MD033 -->
|
|
95
|
+
<summary>See types defined for <code>User</code> example below.</summary>
|
|
96
|
+
|
|
97
|
+
For the purposes of this example the types are deifind in one file shown below.
|
|
98
|
+
|
|
99
|
+
In a production ready application these types would be defined `common`/`shared` folders and `Locale` would be in an `i18n` related file. The files don't _only_ relate to the `User` interfaceb but for the purposes of this example they do.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// src/core/types/user.ts
|
|
103
|
+
|
|
104
|
+
export interface Media {
|
|
105
|
+
id?: string;
|
|
106
|
+
url: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export enum UserState {
|
|
110
|
+
DELETED = 'DELETED',
|
|
111
|
+
BLOCKED = 'BLOCKED',
|
|
112
|
+
INVITED = 'INVITED',
|
|
113
|
+
ACTIVE = 'ACTIVE',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export enum UserType {
|
|
117
|
+
USER = 'USER',
|
|
118
|
+
ADMIN = 'ADMIN',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface User {
|
|
122
|
+
id: string;
|
|
123
|
+
firstName?: string;
|
|
124
|
+
lastName?: string;
|
|
125
|
+
type: UserType;
|
|
126
|
+
state: UserState;
|
|
127
|
+
email: string;
|
|
128
|
+
phone?: string;
|
|
129
|
+
profilePicture?: Media;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// src/core/data/profile/index.ts
|
|
137
|
+
import { useState } from 'react';
|
|
138
|
+
import { createNamedContainer } from 'react-container-kit';
|
|
139
|
+
import type { User } from 'core/types/user';
|
|
140
|
+
|
|
141
|
+
export type ProfileDraft = Pick<User, 'firstName' | 'lastName' | 'phone' | 'language'>;
|
|
142
|
+
|
|
143
|
+
const defaultUser: User = {
|
|
144
|
+
id: '',
|
|
145
|
+
firstName: '',
|
|
146
|
+
lastName: '',
|
|
147
|
+
email: '',
|
|
148
|
+
phone: '',
|
|
149
|
+
type: UserType.USER,
|
|
150
|
+
state: UserState.ACTIVE,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
interface UseProfile {
|
|
154
|
+
profile: User;
|
|
155
|
+
update: (draft: ProfileDraft) => Promise<void>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function useProfileInternal(initialState?: User): UseProfile {
|
|
159
|
+
const [profile, setProfile] = useState<User>(initialState || defaultUser);
|
|
160
|
+
|
|
161
|
+
async function update(draft: ProfileDraft) {
|
|
162
|
+
try {
|
|
163
|
+
const res = await updateProfile(draft);
|
|
164
|
+
setProfile(res);
|
|
165
|
+
return res;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error(e);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { profile, update };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { Provider: ProfileProvider, useContainer: useProfile } =
|
|
177
|
+
createNamedContainer('Profile', useProfileInternal);
|
|
178
|
+
|
|
179
|
+
export { ProfileProvider };
|
|
180
|
+
export default useProfile;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## In depth - When and why to use this package
|
|
184
|
+
|
|
185
|
+
### `createNamedContainer`
|
|
186
|
+
|
|
187
|
+
React DevTools identifies components in the tree by their `displayName`. Without it, every container's provider renders as the generic label `"Provider"` — in an app with several containers, the component tree becomes a wall of identical entries that tells you nothing about which context is which.
|
|
188
|
+
|
|
189
|
+
`createNamedContainer` is a drop-in replacement for `createContainer` that accepts a name as its first argument and uses it to label the provider in DevTools. Pass `'Profile'` and the provider appears as `"ProfileProvider"`, matching the naming convention used by libraries like React Router and React Query. The returned container is otherwise identical in shape: the same `{ Provider, useContainer }` pair, the same TypeScript generics, the same `initialState` support.
|
|
190
|
+
|
|
191
|
+
The name also surfaces in error messages. When `useContainer` is called outside its provider, React includes the component name in the error, so `"ProfileProvider"` in the stack trace points you directly to the missing wrapper rather than leaving you to work out which of your many `"Provider"` components is absent.
|
|
192
|
+
|
|
193
|
+
Use `createNamedContainer` by default for all containers — there is no downside, and the DevTools clarity pays off immediately once your component tree grows beyond a handful of providers.
|
|
194
|
+
|
|
195
|
+
### `composeProviders`
|
|
196
|
+
|
|
197
|
+
As an application grows, it is common for many providers to wrap the same subtree. Three containers already requires three levels of JSX nesting; a real app with auth, theming, routing, snackbars, and feature flags can easily reach ten. This nesting is structural boilerplate — it adds indentation and diff noise without conveying any intent.
|
|
198
|
+
|
|
199
|
+
`composeProviders(...providers)` collapses any number of provider components into a single wrapper. The order is outermost-first, reading left to right: `composeProviders(A, B, C)` is equivalent to `<A><B><C>{children}</C></B></A>`. The resulting component is assigned a `displayName` derived from its members, so DevTools shows the composition rather than hiding it.
|
|
200
|
+
|
|
201
|
+
The constraint to keep in mind: `composeProviders` renders each provider with only `children` — no other props. It is the right tool for providers that are self-contained and initialise from module-level config or internal defaults (themes, routing, snackbars, analytics). For providers that need `initialState` derived from runtime data — for example, seeding a user container after an auth response — keep those in JSX where you can pass the prop directly. In practice, one or two dynamic providers in JSX alongside a single `AppProviders = composeProviders(...)` is the common pattern.
|
|
202
|
+
|
|
203
|
+
### Type utilities
|
|
204
|
+
|
|
205
|
+
**`ContainerValue<C>`** extracts the return type of a container's `useContainer` — the value your hook hands to consuming components. This lets you name and reuse that type across your codebase without importing or re-declaring the hook. It is the canonical way to derive types from a container while keeping its internals encapsulated.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
type ProfileValue = ContainerValue<typeof ProfileContainer>;
|
|
209
|
+
// -> { profile: Profile; setProfile: Dispatch<SetStateAction<Profile>> }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**`ContainerState<C>`** extracts the `initialState` prop type from a container's `Provider`. Useful when writing test helpers or factory functions that need to accept or pass `initialState` without reaching into the container's hook directly.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
type ProfileInitialState = ContainerState<typeof ProfileContainer>;
|
|
216
|
+
// -> Profile
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**`TypedContainer<Value, State>`** is a structural interface describing the shape every container conforms to: a `Provider` component and a `useContainer` hook. Use it to type function parameters or variables that should accept any container without coupling to a specific one.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
function wrapContainer<V>(container: TypedContainer<V>) { ... }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**`AnyProvider<P>`** types any React component that accepts `children` plus any additional props `P`. Useful when building utilities that work with arbitrary provider components, regardless of their own prop requirements.
|
|
226
|
+
|
|
227
|
+
## Tests
|
|
228
|
+
|
|
229
|
+
The test suite uses [Vitest](https://vitest.dev/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) and [happy-dom](https://github.com/capricorn86/happy-dom) for the DOM environment. Compile-time type assertions are validated separately with `tsc`.
|
|
230
|
+
|
|
231
|
+
Run the suite:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
yarn test # vitest run (all runtime tests)
|
|
235
|
+
yarn test:types # tsc --noEmit on *.test-d.ts files
|
|
236
|
+
yarn test:coverage # vitest with v8 coverage
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Test files
|
|
240
|
+
|
|
241
|
+
| File | Description | Tests | Coverage |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `__tests__/create-named-container.test.ts` | Return shape, `displayName` assignment, `useContainer` integration, container independence | 14 | 100% |
|
|
244
|
+
| `__tests__/compose-providers.test.ts` | Return value, provider ordering, context chaining, children rendering, edge cases | 18 | 100% |
|
|
245
|
+
| `__tests__/index.test.ts` | Public API export presence and basic callability | 6 | — |
|
|
246
|
+
| `__tests__/types.test-d.ts` | Compile-time assertions for `ContainerValue`, `ContainerState`, `TypedContainer`, `AnyProvider` | 21 | — |
|
|
247
|
+
|
|
248
|
+
**Total: 38 runtime tests passing · 100% statement/branch/function/line coverage on runtime source**
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as unstated_next from 'unstated-next';
|
|
2
|
+
export { Container, createContainer } from 'unstated-next';
|
|
3
|
+
import { ComponentType, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an unstated-next container and sets a displayName on the Provider
|
|
7
|
+
* for improved React DevTools readability.
|
|
8
|
+
*
|
|
9
|
+
* Without a displayName, every container's Provider appears as "Provider" in
|
|
10
|
+
* the React component tree. This wrapper names it "<name>Provider" so that
|
|
11
|
+
* e.g. `createNamedContainer('Profile', useProfile)` shows as "ProfileProvider"
|
|
12
|
+
* in DevTools.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Instead of:
|
|
16
|
+
* const container = createContainer(useProfile);
|
|
17
|
+
* export const ProfileProvider = container.Provider; // shows as "Provider" in DevTools
|
|
18
|
+
* export const useProfile = container.useContainer;
|
|
19
|
+
*
|
|
20
|
+
* // Use:
|
|
21
|
+
* const { Provider: ProfileProvider, useContainer: useProfile } =
|
|
22
|
+
* createNamedContainer('Profile', useProfile);
|
|
23
|
+
* // ProfileProvider now shows as "ProfileProvider" in DevTools
|
|
24
|
+
*/
|
|
25
|
+
declare function createNamedContainer<Value, State = void>(name: string, useHook: (initialState?: State) => Value): unstated_next.Container<Value, State>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Any React component that accepts children and optional additional props.
|
|
29
|
+
*/
|
|
30
|
+
type AnyProvider<P extends Record<string, unknown> = Record<string, unknown>> = ComponentType<P & {
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Composes multiple provider components into a single wrapper component.
|
|
35
|
+
* Providers are applied outermost-first (left to right in the argument list).
|
|
36
|
+
*
|
|
37
|
+
* This eliminates the deeply-nested JSX pattern common when multiple
|
|
38
|
+
* unstated-next containers need to wrap the same subtree.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Before: deeply nested JSX
|
|
42
|
+
* <SnackbarProvider>
|
|
43
|
+
* <ProfileProvider initialState={profile}>
|
|
44
|
+
* <OrganizationProvider initialState={org}>
|
|
45
|
+
* {children}
|
|
46
|
+
* </OrganizationProvider>
|
|
47
|
+
* </ProfileProvider>
|
|
48
|
+
* </SnackbarProvider>
|
|
49
|
+
*
|
|
50
|
+
* // After: compose providers without dynamic props
|
|
51
|
+
* const AppProviders = composeProviders(
|
|
52
|
+
* SnackbarProvider,
|
|
53
|
+
* ThemeProvider,
|
|
54
|
+
* RouterProvider,
|
|
55
|
+
* );
|
|
56
|
+
* // <AppProviders>{children}</AppProviders>
|
|
57
|
+
*/
|
|
58
|
+
declare function composeProviders(...providers: ComponentType<{
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
}>[]): ComponentType<{
|
|
61
|
+
children?: ReactNode;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extracts the value (return type of useContainer) from a container.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const SnackbarContainer = createContainer(useSnackbar);
|
|
69
|
+
* type SnackbarValue = ContainerValue<typeof SnackbarContainer>;
|
|
70
|
+
* // -> [Queue, SnackbarState]
|
|
71
|
+
*/
|
|
72
|
+
type ContainerValue<C> = C extends {
|
|
73
|
+
useContainer: () => infer V;
|
|
74
|
+
} ? V : never;
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the initialState type from a container's Provider.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const ProfileContainer = createContainer(useProfile);
|
|
80
|
+
* type ProfileState = ContainerState<typeof ProfileContainer>;
|
|
81
|
+
* // -> User
|
|
82
|
+
*/
|
|
83
|
+
type ContainerState<C> = C extends {
|
|
84
|
+
Provider: ComponentType<infer P>;
|
|
85
|
+
} ? P extends {
|
|
86
|
+
initialState?: infer S;
|
|
87
|
+
} ? S : never : never;
|
|
88
|
+
/**
|
|
89
|
+
* A fully-typed container shape returned by createContainer.
|
|
90
|
+
* Useful for typing variables that hold any container.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* function wrapContainer<V, S>(container: TypedContainer<V, S>) { ... }
|
|
94
|
+
*/
|
|
95
|
+
interface TypedContainer<Value, State = void> {
|
|
96
|
+
Provider: ComponentType<{
|
|
97
|
+
initialState?: State;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
}>;
|
|
100
|
+
useContainer: () => Value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { type AnyProvider, type ContainerState, type ContainerValue, type TypedContainer, composeProviders, createNamedContainer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as unstated_next from 'unstated-next';
|
|
2
|
+
export { Container, createContainer } from 'unstated-next';
|
|
3
|
+
import { ComponentType, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an unstated-next container and sets a displayName on the Provider
|
|
7
|
+
* for improved React DevTools readability.
|
|
8
|
+
*
|
|
9
|
+
* Without a displayName, every container's Provider appears as "Provider" in
|
|
10
|
+
* the React component tree. This wrapper names it "<name>Provider" so that
|
|
11
|
+
* e.g. `createNamedContainer('Profile', useProfile)` shows as "ProfileProvider"
|
|
12
|
+
* in DevTools.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Instead of:
|
|
16
|
+
* const container = createContainer(useProfile);
|
|
17
|
+
* export const ProfileProvider = container.Provider; // shows as "Provider" in DevTools
|
|
18
|
+
* export const useProfile = container.useContainer;
|
|
19
|
+
*
|
|
20
|
+
* // Use:
|
|
21
|
+
* const { Provider: ProfileProvider, useContainer: useProfile } =
|
|
22
|
+
* createNamedContainer('Profile', useProfile);
|
|
23
|
+
* // ProfileProvider now shows as "ProfileProvider" in DevTools
|
|
24
|
+
*/
|
|
25
|
+
declare function createNamedContainer<Value, State = void>(name: string, useHook: (initialState?: State) => Value): unstated_next.Container<Value, State>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Any React component that accepts children and optional additional props.
|
|
29
|
+
*/
|
|
30
|
+
type AnyProvider<P extends Record<string, unknown> = Record<string, unknown>> = ComponentType<P & {
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Composes multiple provider components into a single wrapper component.
|
|
35
|
+
* Providers are applied outermost-first (left to right in the argument list).
|
|
36
|
+
*
|
|
37
|
+
* This eliminates the deeply-nested JSX pattern common when multiple
|
|
38
|
+
* unstated-next containers need to wrap the same subtree.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Before: deeply nested JSX
|
|
42
|
+
* <SnackbarProvider>
|
|
43
|
+
* <ProfileProvider initialState={profile}>
|
|
44
|
+
* <OrganizationProvider initialState={org}>
|
|
45
|
+
* {children}
|
|
46
|
+
* </OrganizationProvider>
|
|
47
|
+
* </ProfileProvider>
|
|
48
|
+
* </SnackbarProvider>
|
|
49
|
+
*
|
|
50
|
+
* // After: compose providers without dynamic props
|
|
51
|
+
* const AppProviders = composeProviders(
|
|
52
|
+
* SnackbarProvider,
|
|
53
|
+
* ThemeProvider,
|
|
54
|
+
* RouterProvider,
|
|
55
|
+
* );
|
|
56
|
+
* // <AppProviders>{children}</AppProviders>
|
|
57
|
+
*/
|
|
58
|
+
declare function composeProviders(...providers: ComponentType<{
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
}>[]): ComponentType<{
|
|
61
|
+
children?: ReactNode;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extracts the value (return type of useContainer) from a container.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const SnackbarContainer = createContainer(useSnackbar);
|
|
69
|
+
* type SnackbarValue = ContainerValue<typeof SnackbarContainer>;
|
|
70
|
+
* // -> [Queue, SnackbarState]
|
|
71
|
+
*/
|
|
72
|
+
type ContainerValue<C> = C extends {
|
|
73
|
+
useContainer: () => infer V;
|
|
74
|
+
} ? V : never;
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the initialState type from a container's Provider.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const ProfileContainer = createContainer(useProfile);
|
|
80
|
+
* type ProfileState = ContainerState<typeof ProfileContainer>;
|
|
81
|
+
* // -> User
|
|
82
|
+
*/
|
|
83
|
+
type ContainerState<C> = C extends {
|
|
84
|
+
Provider: ComponentType<infer P>;
|
|
85
|
+
} ? P extends {
|
|
86
|
+
initialState?: infer S;
|
|
87
|
+
} ? S : never : never;
|
|
88
|
+
/**
|
|
89
|
+
* A fully-typed container shape returned by createContainer.
|
|
90
|
+
* Useful for typing variables that hold any container.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* function wrapContainer<V, S>(container: TypedContainer<V, S>) { ... }
|
|
94
|
+
*/
|
|
95
|
+
interface TypedContainer<Value, State = void> {
|
|
96
|
+
Provider: ComponentType<{
|
|
97
|
+
initialState?: State;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
}>;
|
|
100
|
+
useContainer: () => Value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { type AnyProvider, type ContainerState, type ContainerValue, type TypedContainer, composeProviders, createNamedContainer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
composeProviders: () => composeProviders,
|
|
24
|
+
createContainer: () => import_unstated_next2.createContainer,
|
|
25
|
+
createNamedContainer: () => createNamedContainer
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_unstated_next2 = require("unstated-next");
|
|
29
|
+
|
|
30
|
+
// src/create-named-container.ts
|
|
31
|
+
var import_unstated_next = require("unstated-next");
|
|
32
|
+
function createNamedContainer(name, useHook) {
|
|
33
|
+
const container = (0, import_unstated_next.createContainer)(useHook);
|
|
34
|
+
container.Provider.displayName = `${name}Provider`;
|
|
35
|
+
return container;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/compose-providers.ts
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
function composeProviders(...providers) {
|
|
41
|
+
function ComposedProviders({ children }) {
|
|
42
|
+
return providers.reduceRight(
|
|
43
|
+
(child, Provider) => (0, import_react.createElement)(Provider, null, child),
|
|
44
|
+
children
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
ComposedProviders.displayName = `ComposedProviders(${providers.map((p) => p.displayName || p.name || "Unknown").join(", ")})`;
|
|
48
|
+
return ComposedProviders;
|
|
49
|
+
}
|
|
50
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
51
|
+
0 && (module.exports = {
|
|
52
|
+
composeProviders,
|
|
53
|
+
createContainer,
|
|
54
|
+
createNamedContainer
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/create-named-container.ts","../src/compose-providers.ts"],"sourcesContent":["// Re-export the core unstated-next API so consumers only need this package\nexport { createContainer } from 'unstated-next';\nexport type { Container } from 'unstated-next';\n\n// Enhanced container factory with React DevTools displayName support\nexport { createNamedContainer } from './create-named-container';\n\n// Provider composition utility\nexport { composeProviders } from './compose-providers';\nexport type { AnyProvider } from './compose-providers';\n\n// TypeScript utility types for working with containers\nexport type { ContainerValue, ContainerState, TypedContainer } from './types';\n","import { createContainer } from 'unstated-next';\n\n/**\n * Creates an unstated-next container and sets a displayName on the Provider\n * for improved React DevTools readability.\n *\n * Without a displayName, every container's Provider appears as \"Provider\" in\n * the React component tree. This wrapper names it \"<name>Provider\" so that\n * e.g. `createNamedContainer('Profile', useProfile)` shows as \"ProfileProvider\"\n * in DevTools.\n *\n * @example\n * // Instead of:\n * const container = createContainer(useProfile);\n * export const ProfileProvider = container.Provider; // shows as \"Provider\" in DevTools\n * export const useProfile = container.useContainer;\n *\n * // Use:\n * const { Provider: ProfileProvider, useContainer: useProfile } =\n * createNamedContainer('Profile', useProfile);\n * // ProfileProvider now shows as \"ProfileProvider\" in DevTools\n */\nexport function createNamedContainer<Value, State = void>(\n name: string,\n useHook: (initialState?: State) => Value,\n) {\n const container = createContainer<Value, State>(useHook);\n container.Provider.displayName = `${name}Provider`;\n return container;\n}\n","import { createElement, type ComponentType, type ReactElement, type ReactNode } from 'react';\n\n/**\n * Any React component that accepts children and optional additional props.\n */\nexport type AnyProvider<P extends Record<string, unknown> = Record<string, unknown>> =\n ComponentType<P & { children?: ReactNode }>;\n\n/**\n * Composes multiple provider components into a single wrapper component.\n * Providers are applied outermost-first (left to right in the argument list).\n *\n * This eliminates the deeply-nested JSX pattern common when multiple\n * unstated-next containers need to wrap the same subtree.\n *\n * @example\n * // Before: deeply nested JSX\n * <SnackbarProvider>\n * <ProfileProvider initialState={profile}>\n * <OrganizationProvider initialState={org}>\n * {children}\n * </OrganizationProvider>\n * </ProfileProvider>\n * </SnackbarProvider>\n *\n * // After: compose providers without dynamic props\n * const AppProviders = composeProviders(\n * SnackbarProvider,\n * ThemeProvider,\n * RouterProvider,\n * );\n * // <AppProviders>{children}</AppProviders>\n */\nexport function composeProviders(\n ...providers: ComponentType<{ children?: ReactNode }>[]\n): ComponentType<{ children?: ReactNode }> {\n function ComposedProviders({ children }: { children?: ReactNode }): ReactElement {\n return providers.reduceRight<ReactElement>(\n (child, Provider) => createElement(Provider, null, child),\n children as ReactElement,\n );\n }\n\n ComposedProviders.displayName = `ComposedProviders(${providers\n .map((p) => p.displayName || p.name || 'Unknown')\n .join(', ')})`;\n\n return ComposedProviders;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,IAAAA,wBAAgC;;;ACDhC,2BAAgC;AAsBzB,SAAS,qBACd,MACA,SACA;AACA,QAAM,gBAAY,sCAA8B,OAAO;AACvD,YAAU,SAAS,cAAc,GAAG,IAAI;AACxC,SAAO;AACT;;;AC7BA,mBAAqF;AAiC9E,SAAS,oBACX,WACsC;AACzC,WAAS,kBAAkB,EAAE,SAAS,GAA2C;AAC/E,WAAO,UAAU;AAAA,MACf,CAAC,OAAO,iBAAa,4BAAc,UAAU,MAAM,KAAK;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,oBAAkB,cAAc,qBAAqB,UAClD,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,SAAS,EAC/C,KAAK,IAAI,CAAC;AAEb,SAAO;AACT;","names":["import_unstated_next"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createContainer as createContainer2 } from "unstated-next";
|
|
3
|
+
|
|
4
|
+
// src/create-named-container.ts
|
|
5
|
+
import { createContainer } from "unstated-next";
|
|
6
|
+
function createNamedContainer(name, useHook) {
|
|
7
|
+
const container = createContainer(useHook);
|
|
8
|
+
container.Provider.displayName = `${name}Provider`;
|
|
9
|
+
return container;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/compose-providers.ts
|
|
13
|
+
import { createElement } from "react";
|
|
14
|
+
function composeProviders(...providers) {
|
|
15
|
+
function ComposedProviders({ children }) {
|
|
16
|
+
return providers.reduceRight(
|
|
17
|
+
(child, Provider) => createElement(Provider, null, child),
|
|
18
|
+
children
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
ComposedProviders.displayName = `ComposedProviders(${providers.map((p) => p.displayName || p.name || "Unknown").join(", ")})`;
|
|
22
|
+
return ComposedProviders;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
composeProviders,
|
|
26
|
+
createContainer2 as createContainer,
|
|
27
|
+
createNamedContainer
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/create-named-container.ts","../src/compose-providers.ts"],"sourcesContent":["// Re-export the core unstated-next API so consumers only need this package\nexport { createContainer } from 'unstated-next';\nexport type { Container } from 'unstated-next';\n\n// Enhanced container factory with React DevTools displayName support\nexport { createNamedContainer } from './create-named-container';\n\n// Provider composition utility\nexport { composeProviders } from './compose-providers';\nexport type { AnyProvider } from './compose-providers';\n\n// TypeScript utility types for working with containers\nexport type { ContainerValue, ContainerState, TypedContainer } from './types';\n","import { createContainer } from 'unstated-next';\n\n/**\n * Creates an unstated-next container and sets a displayName on the Provider\n * for improved React DevTools readability.\n *\n * Without a displayName, every container's Provider appears as \"Provider\" in\n * the React component tree. This wrapper names it \"<name>Provider\" so that\n * e.g. `createNamedContainer('Profile', useProfile)` shows as \"ProfileProvider\"\n * in DevTools.\n *\n * @example\n * // Instead of:\n * const container = createContainer(useProfile);\n * export const ProfileProvider = container.Provider; // shows as \"Provider\" in DevTools\n * export const useProfile = container.useContainer;\n *\n * // Use:\n * const { Provider: ProfileProvider, useContainer: useProfile } =\n * createNamedContainer('Profile', useProfile);\n * // ProfileProvider now shows as \"ProfileProvider\" in DevTools\n */\nexport function createNamedContainer<Value, State = void>(\n name: string,\n useHook: (initialState?: State) => Value,\n) {\n const container = createContainer<Value, State>(useHook);\n container.Provider.displayName = `${name}Provider`;\n return container;\n}\n","import { createElement, type ComponentType, type ReactElement, type ReactNode } from 'react';\n\n/**\n * Any React component that accepts children and optional additional props.\n */\nexport type AnyProvider<P extends Record<string, unknown> = Record<string, unknown>> =\n ComponentType<P & { children?: ReactNode }>;\n\n/**\n * Composes multiple provider components into a single wrapper component.\n * Providers are applied outermost-first (left to right in the argument list).\n *\n * This eliminates the deeply-nested JSX pattern common when multiple\n * unstated-next containers need to wrap the same subtree.\n *\n * @example\n * // Before: deeply nested JSX\n * <SnackbarProvider>\n * <ProfileProvider initialState={profile}>\n * <OrganizationProvider initialState={org}>\n * {children}\n * </OrganizationProvider>\n * </ProfileProvider>\n * </SnackbarProvider>\n *\n * // After: compose providers without dynamic props\n * const AppProviders = composeProviders(\n * SnackbarProvider,\n * ThemeProvider,\n * RouterProvider,\n * );\n * // <AppProviders>{children}</AppProviders>\n */\nexport function composeProviders(\n ...providers: ComponentType<{ children?: ReactNode }>[]\n): ComponentType<{ children?: ReactNode }> {\n function ComposedProviders({ children }: { children?: ReactNode }): ReactElement {\n return providers.reduceRight<ReactElement>(\n (child, Provider) => createElement(Provider, null, child),\n children as ReactElement,\n );\n }\n\n ComposedProviders.displayName = `ComposedProviders(${providers\n .map((p) => p.displayName || p.name || 'Unknown')\n .join(', ')})`;\n\n return ComposedProviders;\n}\n"],"mappings":";AACA,SAAS,mBAAAA,wBAAuB;;;ACDhC,SAAS,uBAAuB;AAsBzB,SAAS,qBACd,MACA,SACA;AACA,QAAM,YAAY,gBAA8B,OAAO;AACvD,YAAU,SAAS,cAAc,GAAG,IAAI;AACxC,SAAO;AACT;;;AC7BA,SAAS,qBAA4E;AAiC9E,SAAS,oBACX,WACsC;AACzC,WAAS,kBAAkB,EAAE,SAAS,GAA2C;AAC/E,WAAO,UAAU;AAAA,MACf,CAAC,OAAO,aAAa,cAAc,UAAU,MAAM,KAAK;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,oBAAkB,cAAc,qBAAqB,UAClD,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,SAAS,EAC/C,KAAK,IAAI,CAAC;AAEb,SAAO;AACT;","names":["createContainer"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-container-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript utilities for the React container pattern — named containers, provider composition, and type helpers",
|
|
5
|
+
"packageManager": "yarn@4.13.0",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:types": "tsc --project tsconfig.types-test.json --noEmit",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"prepublishOnly": "yarn build && yarn typecheck && yarn test && yarn test:types",
|
|
28
|
+
"release": "yarn npm publish"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=16.8.0",
|
|
32
|
+
"unstated-next": "^1.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@testing-library/dom": "^10.0.0",
|
|
36
|
+
"@testing-library/react": "^16.0.0",
|
|
37
|
+
"@types/react": "^18.0.0",
|
|
38
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
39
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
40
|
+
"happy-dom": "^14.0.0",
|
|
41
|
+
"react": "^18.0.0",
|
|
42
|
+
"react-dom": "^18.0.0",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"unstated-next": "^1.1.0",
|
|
46
|
+
"vite": "^5.0.0",
|
|
47
|
+
"vitest": "^2.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|