From 34ea976cd184b6a8efea0ba4fcbc08283baa6b07 Mon Sep 17 00:00:00 2001 From: Olexii Kyrychenko Date: Fri, 18 Sep 2026 22:59:43 +0300 Subject: [PATCH] deprecate React 19 helpers --- CHANGELOG.md | 3 + README.md | 76 +++++++++++++------ src/index.ts | 1 + .../__tests__/createTransitionAction.test.ts | 1 + .../__tests__/useActionStateAdapter.test.tsx | 1 + .../__tests__/useOptimisticReducer.test.tsx | 1 + src/react19/createTransitionAction.ts | 4 + src/react19/index.ts | 3 + src/react19/useActionStateAdapter.ts | 4 + src/react19/useOptimisticReducer.ts | 3 + 10 files changed, 74 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de20823..4d74e26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 93f01ef..e43dbed 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/index.ts b/src/index.ts index fa0a49e..019e36c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/react19/__tests__/createTransitionAction.test.ts b/src/react19/__tests__/createTransitionAction.test.ts index ad46873..5dc7fda 100644 --- a/src/react19/__tests__/createTransitionAction.test.ts +++ b/src/react19/__tests__/createTransitionAction.test.ts @@ -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"; diff --git a/src/react19/__tests__/useActionStateAdapter.test.tsx b/src/react19/__tests__/useActionStateAdapter.test.tsx index 6715734..23b04ac 100644 --- a/src/react19/__tests__/useActionStateAdapter.test.tsx +++ b/src/react19/__tests__/useActionStateAdapter.test.tsx @@ -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"; diff --git a/src/react19/__tests__/useOptimisticReducer.test.tsx b/src/react19/__tests__/useOptimisticReducer.test.tsx index 3ec6445..947c0f6 100644 --- a/src/react19/__tests__/useOptimisticReducer.test.tsx +++ b/src/react19/__tests__/useOptimisticReducer.test.tsx @@ -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"; diff --git a/src/react19/createTransitionAction.ts b/src/react19/createTransitionAction.ts index 7ef61ce..798379c 100644 --- a/src/react19/createTransitionAction.ts +++ b/src/react19/createTransitionAction.ts @@ -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>( action: (...args: TArgs) => void | Promise diff --git a/src/react19/index.ts b/src/react19/index.ts index 156c67a..4510934 100644 --- a/src/react19/index.ts +++ b/src/react19/index.ts @@ -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"; diff --git a/src/react19/useActionStateAdapter.ts b/src/react19/useActionStateAdapter.ts index ae62f14..1dd6c94 100644 --- a/src/react19/useActionStateAdapter.ts +++ b/src/react19/useActionStateAdapter.ts @@ -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( action: (payload: TPayload) => Awaited | Promise>, diff --git a/src/react19/useOptimisticReducer.ts b/src/react19/useOptimisticReducer.ts index 2d1c287..9fee639 100644 --- a/src/react19/useOptimisticReducer.ts +++ b/src/react19/useOptimisticReducer.ts @@ -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( committedState: TState,