Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Removed

- Removed deprecated provider, resolved-store, and lifecycle aliases.

## [0.4.2] - 2026-07-25

### Fixed
Expand Down
133 changes: 62 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ If you want Zustand Redux DevTools, apply `devtools(...)` in the store creator i
- Full TypeScript inference with Zustand middleware support
- React 19 helpers for transitions, optimistic updates, and action state adapters

## Store Access Matrix

Each factory uses the same value/plain/API naming pattern:

| Store source | Shallow-first value | Plain value | Store API |
| ------------ | ------------------- | ----------------------- | --------------------- |
| Global | `useStore` | `useStorePlain` | `useStoreApi` |
| Provider | `useContextStore` | `useContextStorePlain` | `useContextStoreApi` |
| Resolved | `useResolvedValue` | `useResolvedStorePlain` | `useResolvedStoreApi` |

The provider factory also exposes `useContextStoreOptional` for integrations that
need to detect whether a matching provider is present. The toolkit exposes its
shared provider bindings through `provider`; `getProvider()` returns that same
object when function-based access is useful.

## Installation

```bash
Expand Down Expand Up @@ -121,11 +136,17 @@ interface WizardStore {
next: () => void;
}

const { Provider: WizardProvider, useContextStore, useContextStoreApi } =
createStoreProvider<WizardStore>((set) => ({
const {
Provider: WizardProvider,
useContextStore,
useContextStoreApi,
} = createStoreProvider<WizardStore>(
(set) => ({
step: 1,
next: () => set((state) => ({ step: state.step + 1 })),
}), "Wizard");
}),
"Wizard"
);

function WizardStep() {
const step = useContextStore((state) => state.step);
Expand Down Expand Up @@ -162,10 +183,13 @@ interface CartStore {
addItem: (item: string) => void;
}

const cartToolkit = createStoreToolkit<CartStore>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}), { name: "Cart" });
const cartToolkit = createStoreToolkit<CartStore>(
(set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}),
{ name: "Cart" }
);

export const { useResolvedValue: useCart } = cartToolkit;
export const { Provider: CartProvider } = cartToolkit.provider;
Expand All @@ -183,7 +207,6 @@ Returns:
- `useStoreApi`
- `provider`
- `getProvider()`
- `createProvider()` deprecated alias
- `useResolvedStoreApi()`
- `useResolvedValue()`
- `useResolvedStorePlain()`
Expand Down Expand Up @@ -269,9 +292,12 @@ const { useStoreApi } = createShallowStore<PreferencesStore>(() => ({
theme: "light",
}));

const { useContextStoreOptional } = createStoreProvider<PreferencesStore>(() => ({
theme: "light",
}), "Preferences");
const { useContextStoreOptional } = createStoreProvider<PreferencesStore>(
() => ({
theme: "light",
}),
"Preferences"
);

const { useResolvedValue } = createResolvedStoreHooks(useStoreApi, useContextStoreOptional);

Expand All @@ -284,10 +310,28 @@ function ThemeLabel() {

It returns the same resolved hook family used by `createStoreToolkit`:

- `useResolvedStoreApi()` and its deprecated `useResolvedStore()` alias
- `useResolvedValue()` and its deprecated `useResolvedStoreWithSelector()` alias
- `useResolvedStoreApi()`
- `useResolvedValue()`
- `useResolvedStorePlain()`

## Upgrading After Deprecated Alias Removal

The deprecated compatibility names have been removed. Replace them with their
canonical equivalents:

| Removed name | Replacement |
| -------------------------------- | --------------------------------------------- |
| `toolkit.createProvider()` | `toolkit.provider` or `toolkit.getProvider()` |
| `useContext()` | `useContextStoreApi()` |
| `useOptionalContext()` | `useContextStoreOptional()` |
| `useResolvedStore()` | `useResolvedStoreApi()` |
| `useResolvedStoreWithSelector()` | `useResolvedValue()` |
| `onStoreCreate` | `onStoreReady` |

`onStoreReady` runs after the provider commits and at most once for each provider
store instance. Use `onStoreInit` when state must be initialized synchronously
during store creation.

## Provider Lifecycle

`createStoreProvider` supports two lifecycle stages:
Expand Down Expand Up @@ -317,11 +361,6 @@ const { Provider } = createStoreProvider<AppStore>((set) => ({
</Provider>;
```

Deprecated alias:

- `onStoreCreate` maps to the post-commit `onStoreReady` behavior and is ignored
when `onStoreReady` is also provided

## Middleware Support

Zustand middleware belongs in the store creator.
Expand Down Expand Up @@ -371,10 +410,7 @@ interface FilterStore {
setQuery: (query: string) => void;
}

const { useStoreApi } = createShallowStore<
FilterStore,
[["zustand/subscribeWithSelector", never]]
>(
const { useStoreApi } = createShallowStore<FilterStore, [["zustand/subscribeWithSelector", never]]>(
subscribeWithSelector((set) => ({
query: "",
setQuery: (query) => set({ query }),
Expand All @@ -391,51 +427,6 @@ const unsubscribe = useStoreApi.subscribe(
unsubscribe();
```

## Migration

Deprecated names still exist for compatibility, but new code should prefer the current API.

### Provider access

```tsx
const { Provider } = toolkit.provider;
```

Old aliases:

```tsx
const { Provider } = toolkit.getProvider();
const { Provider: LegacyProvider } = toolkit.createProvider();
```

### Raw provider hooks

```tsx
const store = useContextStoreApi();
const maybeStore = useContextStoreOptional();
```

Old aliases:

```tsx
const store = useContext();
const maybeStore = useOptionalContext();
```

### Resolved hooks

```tsx
const value = useResolvedValue((state) => state.value);
const store = useResolvedStoreApi();
```

Old aliases:

```tsx
const value = useResolvedStoreWithSelector((state) => state.value);
const store = useResolvedStore();
```

## React 19 Helpers

```tsx
Expand All @@ -459,10 +450,10 @@ const [status, submit, isPending] = useActionStateAdapter(async (payload: FormDa
return "saved";
}, "idle");

const [optimisticTodos, addOptimisticTodo] = useOptimisticReducer(
todos,
(current, nextTodo) => [...current, nextTodo]
);
const [optimisticTodos, addOptimisticTodo] = useOptimisticReducer(todos, (current, nextTodo) => [
...current,
nextTodo,
]);
```

`createTransitionAction` supports synchronous and asynchronous actions. In React 19,
Expand Down
38 changes: 6 additions & 32 deletions src/core/__tests__/createStoreToolkit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,20 @@ interface TestStore {
increment: () => void;
}

interface LegacyToolkitApi {
createProvider: () => { Provider: (props: { children: ReactNode }) => ReactNode };
useResolvedStore: () => unknown;
useResolvedStoreWithSelector: <T>(
selector: (state: TestStore) => T,
equalityFn?: (a: T, b: T) => boolean
) => T;
}

describe("createStoreToolkit", () => {
it("should create complete toolkit", () => {
const toolkit = createStoreToolkit<TestStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

expect(toolkit.useStore).toBeDefined();
expect(toolkit.useStorePlain).toBeDefined();
expect(toolkit.useStoreApi).toBeDefined();
expect(toolkit.provider).toBeDefined();
expect(toolkit.getProvider).toBeDefined();
expect(legacyToolkit.createProvider).toBeDefined();
expect(toolkit.useResolvedStoreApi).toBeDefined();
expect(legacyToolkit.useResolvedStore).toBeDefined();
expect(toolkit.useResolvedValue).toBeDefined();
expect(legacyToolkit.useResolvedStoreWithSelector).toBeDefined();
expect(toolkit.useResolvedStorePlain).toBeDefined();
});

Expand All @@ -45,10 +32,7 @@ describe("createStoreToolkit", () => {
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

expect(toolkit.provider).toBe(toolkit.getProvider());
expect(toolkit.getProvider()).toBe(legacyToolkit.createProvider());
});

it("should work with global store", () => {
Expand Down Expand Up @@ -83,16 +67,13 @@ describe("createStoreToolkit", () => {
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

const { Provider } = toolkit.provider;

const wrapper = ({ children }: { children: ReactNode }) => <Provider>{children}</Provider>;

const { result } = renderHook(
() => legacyToolkit.useResolvedStoreWithSelector((state) => state.count),
{ wrapper }
);
const { result } = renderHook(() => toolkit.useResolvedValue((state) => state.count), {
wrapper,
});

expect(result.current).toBe(0);
});
Expand All @@ -102,20 +83,18 @@ describe("createStoreToolkit", () => {
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

const { Provider } = toolkit.provider;

// Global store
const { result: globalResult } = renderHook(() =>
legacyToolkit.useResolvedStoreWithSelector((state) => state.count)
toolkit.useResolvedValue((state) => state.count)
);

// Provider store
const wrapper = ({ children }: { children: ReactNode }) => <Provider>{children}</Provider>;

const { result: providerResult } = renderHook(
() => legacyToolkit.useResolvedStoreWithSelector((state) => state.count),
() => toolkit.useResolvedValue((state) => state.count),
{ wrapper }
);

Expand All @@ -138,10 +117,8 @@ describe("createStoreToolkit", () => {
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

const { result } = renderHook(() =>
legacyToolkit.useResolvedStoreWithSelector(
toolkit.useResolvedValue(
(state) => state.count,
() => true
)
Expand All @@ -161,14 +138,11 @@ describe("createStoreToolkit", () => {
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const legacyToolkit: LegacyToolkitApi = toolkit;

const { result } = renderHook(() => toolkit.useResolvedStorePlain((state) => state.count));
const { result: apiResult } = renderHook(() => toolkit.useResolvedStoreApi());

expect(result.current).toBe(0);
expect(apiResult.current).toBe(toolkit.useStoreApi);
expect(toolkit.useResolvedValue).toBe(legacyToolkit.useResolvedStoreWithSelector);
});

it("should expose full resolved state without selector outside provider", () => {
Expand Down
8 changes: 2 additions & 6 deletions src/core/createShallowStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { createStore } from "zustand";
import { useStoreSelection, useStoreSelectionPlain } from "../hooks";
import type {
MutatorsStateCreator,
ShallowStoreBindings,
StoreApiWithMutators,
StoreMutatorTuple,
} from "../types";
import type { MutatorsStateCreator, StoreApiWithMutators, StoreMutatorTuple } from "../types";
import type { ShallowStoreBindings } from "./createShallowStore.types";

/**
* Creates a Zustand store with shallow-first selector semantics.
Expand Down
9 changes: 9 additions & 0 deletions src/core/createShallowStore.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { StorePlainHook, StoreValueHook } from "../hooks";
import type { StoreApiWithMutators, StoreMutatorTuple } from "../types";

/** Store bindings with shallow comparison built in. */
export interface ShallowStoreBindings<TState, TMutators extends Array<StoreMutatorTuple> = []> {
useStore: StoreValueHook<TState>;
useStorePlain: StorePlainHook<TState>;
useStoreApi: StoreApiWithMutators<TState, TMutators>;
}
Loading
Loading