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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Standardized package development, CI, and release workflows on pnpm 11.
- Adopted `@okyrychenko-dev/type-utils` as a direct runtime dependency for shared guards.
- Aligned package exports and validation tooling with the other maintained `@okyrychenko-dev` libraries.
- Deprecated `createTransitionAction`, `useActionStateAdapter`, and `useOptimisticReducer` in favor
of composing React's supported primitives directly. The helpers remain runtime-compatible without
warnings, and removal is deferred to the next intentional major release.
- Deprecated `createStoreToolkit().getProvider()` in favor of the canonical `provider` property.
The compatibility accessor still returns the same Provider bindings without runtime warnings;
removal is deferred to the next intentional major release.
Expand Down
76 changes: 53 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ If you want Zustand Redux DevTools, apply `devtools(...)` in the store creator i
- Resolved hooks that choose context store first and fall back to global store
- Optional custom equality for shallow-first selector hooks
- Full TypeScript inference with Zustand middleware support
- React 19 helpers for transitions, optimistic updates, and action state adapters

## Store Access Matrix

Expand Down Expand Up @@ -433,39 +432,70 @@ const unsubscribe = useStoreApi.subscribe(
unsubscribe();
```

## React 19 Helpers
## Migrating from the Deprecated React 19 Helpers

`createTransitionAction`, `useActionStateAdapter`, and `useOptimisticReducer`
remain available for compatibility until the next intentional major release.
They emit no runtime warnings. New code should compose React's supported
primitives directly.

Replace `createTransitionAction` with `startTransition`. Return an asynchronous
action's promise from the transition scope so React keeps the transition pending
until the action settles:

```tsx
import {
createTransitionAction,
useActionStateAdapter,
useOptimisticReducer,
} from "@okyrychenko-dev/react-zustand-toolkit";
import { startTransition } from "react";

const incrementInTransition = createTransitionAction(() => {
counterToolkit.useStoreApi.getState().increment();
});
function incrementInTransition(): void {
startTransition(() => {
counterToolkit.useStoreApi.getState().increment();
});
}

const saveInTransition = createTransitionAction(async () => {
await save();
counterToolkit.useStoreApi.setState({ saved: true });
});
function saveInTransition(): void {
startTransition(async () => {
await save();
counterToolkit.useStoreApi.setState({ saved: true });
});
}
```

const [status, submit, isPending] = useActionStateAdapter(async (payload: FormData) => {
await save(payload);
return "saved";
}, "idle");
Replace `useActionStateAdapter` with `useActionState`. Define the reducer from
the current `action` during each render so a re-render uses the latest action:

```tsx
import { useActionState } from "react";

const [optimisticTodos, addOptimisticTodo] = useOptimisticReducer(todos, (current, nextTodo) => [
const [status, submit, isPending] = useActionState(
(_previousStatus: string, payload: FormData) => action(payload),
"idle"
);
```

Replace `useOptimisticReducer` with `useOptimistic` and dispatch optimistic
updates within a transition:

```tsx
import { startTransition, useCallback, useOptimistic } from "react";

const [optimisticTodos, dispatchOptimisticTodo] = useOptimistic(todos, (current, nextTodo) => [
...current,
nextTodo,
]);

const addOptimisticTodo = useCallback(
(todo: Todo) => {
startTransition(() => dispatchOptimisticTodo(todo));
},
[dispatchOptimisticTodo]
);
```

`createTransitionAction` supports synchronous and asynchronous actions. In React 19,
an async action remains part of the transition until its returned promise settles.
`useActionStateAdapter` always invokes the latest action supplied to the hook,
including after a re-render.
These helpers are generic React primitive compositions rather than Zustand Store
selection or composition interfaces. Removing them leaves short, direct React
calls instead of spreading domain complexity across consumers. Equivalent thin
wrappers should not be reintroduced unless a future Store-specific requirement
creates a deeper interface.

## Development

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { createStoreProvider } from "./providers";
export { createResolvedStoreHooks } from "./hooks";

// React 19 utilities
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Preserves compatibility exports until the next major release.
export { createTransitionAction, useActionStateAdapter, useOptimisticReducer } from "./react19";

// Types
Expand Down
1 change: 1 addition & 0 deletions src/react19/__tests__/createTransitionAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-deprecated -- Characterizes the deprecated compatibility helper. */
import { startTransition } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createTransitionAction } from "../createTransitionAction";
Expand Down
1 change: 1 addition & 0 deletions src/react19/__tests__/useActionStateAdapter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-deprecated -- Characterizes the deprecated compatibility helper. */
import { act, renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useActionStateAdapter } from "../useActionStateAdapter";
Expand Down
1 change: 1 addition & 0 deletions src/react19/__tests__/useOptimisticReducer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-deprecated -- Characterizes the deprecated compatibility helper. */
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useOptimisticReducer } from "../useOptimisticReducer";
Expand Down
4 changes: 4 additions & 0 deletions src/react19/createTransitionAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { startTransition } from "react";
*
* Async action promises are returned from the transition scope so React 19 can
* keep the transition pending until the action settles.
*
* @deprecated Use React's `startTransition(() => action(...args))` directly. Return an async
* action's promise from the transition scope so the transition remains pending until it settles.
* This helper will be removed in the next intentional major release.
*/
export function createTransitionAction<TArgs extends Array<unknown>>(
action: (...args: TArgs) => void | Promise<void>
Expand Down
3 changes: 3 additions & 0 deletions src/react19/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Preserves this compatibility export until the next major release.
export { createTransitionAction } from "./createTransitionAction";
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Preserves this compatibility export until the next major release.
export { useActionStateAdapter } from "./useActionStateAdapter";
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Preserves this compatibility export until the next major release.
export { useOptimisticReducer } from "./useOptimisticReducer";
4 changes: 4 additions & 0 deletions src/react19/useActionStateAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { useActionState } from "react";

/**
* Adapter around React 19 useActionState for store-related async actions.
*
* @deprecated Use React's `useActionState` directly with a reducer that invokes the current action.
* Recreate that reducer from the latest action on each render. This helper will be removed in the
* next intentional major release.
*/
export function useActionStateAdapter<TState, TPayload>(
action: (payload: TPayload) => Awaited<TState> | Promise<Awaited<TState>>,
Expand Down
3 changes: 3 additions & 0 deletions src/react19/useOptimisticReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { startTransition, useCallback, useOptimistic } from "react";

/**
* Thin wrapper around React 19 useOptimistic for optimistic store UIs.
*
* @deprecated Use React's `useOptimistic` directly and dispatch optimistic updates inside
* `startTransition`. This helper will be removed in the next intentional major release.
*/
export function useOptimisticReducer<TState, TInput>(
committedState: TState,
Expand Down
Loading