()({
+ initialState: { volume: 1, muted: false },
+ getSnapshot: ({ target }) => ({
+ volume: target.volume,
+ muted: target.muted,
+ }),
+ subscribe: ({ target, update, signal }) => {
+ const handler = () => update();
+ target.addEventListener('volumechange', handler);
+ signal.addEventListener('abort', () => {
+ target.removeEventListener('volumechange', handler);
+ });
+ },
+ request: {
+ setVolume: {
+ handler: async (volume: number, { target }) => {
+ await Promise.resolve();
+ target.volume = volume;
+ target.dispatchEvent(new Event('volumechange'));
+ return volume;
+ },
+ },
+ failingRequest: {
+ handler: async () => {
+ await Promise.resolve();
+ throw new Error('Request failed');
+ },
+ },
+ },
+});
+
+export function createAsyncTestStore() {
+ const store = createCoreStore({
+ slices: [asyncAudioSlice],
+ onError: noop,
+ });
+
+ const target = new AsyncMockMedia();
+ store.attach(target);
+
+ return { store, target };
+}
diff --git a/packages/store/src/react/hooks/tests/use-mutation.test.tsx b/packages/store/src/react/hooks/tests/use-mutation.test.tsx
new file mode 100644
index 00000000..c489721b
--- /dev/null
+++ b/packages/store/src/react/hooks/tests/use-mutation.test.tsx
@@ -0,0 +1,116 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { useMutation } from '../use-mutation';
+import { createAsyncTestStore, createTestStore } from './test-utils';
+
+describe('useMutation', () => {
+ it('returns mutation result with idle status initially', () => {
+ const { store } = createAsyncTestStore();
+
+ const { result } = renderHook(() => useMutation(store, 'setVolume'));
+
+ expect(result.current.status).toBe('idle');
+ expect(typeof result.current.mutate).toBe('function');
+ expect(typeof result.current.reset).toBe('function');
+ });
+
+ it('updates to success status after successful mutation', async () => {
+ const { store } = createAsyncTestStore();
+
+ const { result } = renderHook(() => useMutation(store, 'setVolume'));
+
+ await act(async () => {
+ await result.current.mutate(0.5);
+ });
+
+ expect(result.current.status).toBe('success');
+ if (result.current.status === 'success') {
+ expect(result.current.data).toBe(0.5);
+ }
+ });
+
+ it('updates to error status after failed mutation', async () => {
+ const { store } = createAsyncTestStore();
+
+ const { result } = renderHook(() => useMutation(store, 'failingRequest'));
+
+ await act(async () => {
+ try {
+ await result.current.mutate();
+ } catch {
+ // Expected to throw
+ }
+ });
+
+ expect(result.current.status).toBe('error');
+ if (result.current.status === 'error') {
+ expect(result.current.error).toBeInstanceOf(Error);
+ expect((result.current.error as Error).message).toBe('Request failed');
+ }
+ });
+
+ it('reset clears settled state', async () => {
+ const { store } = createAsyncTestStore();
+
+ const { result } = renderHook(() => useMutation(store, 'setVolume'));
+
+ await act(async () => {
+ await result.current.mutate(0.5);
+ });
+
+ expect(result.current.status).toBe('success');
+
+ await act(async () => {
+ result.current.reset();
+ });
+
+ expect(result.current.status).toBe('idle');
+ });
+
+ it('re-renders only when task status changes', async () => {
+ const { store } = createAsyncTestStore();
+ const renderCount = vi.fn();
+
+ const { result } = renderHook(() => {
+ renderCount();
+ return useMutation(store, 'setVolume');
+ });
+
+ expect(renderCount).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ await result.current.mutate(0.5);
+ });
+
+ // Should have re-rendered for pending and success
+ expect(renderCount.mock.calls.length).toBeGreaterThan(1);
+ });
+
+ it('mutate function is stable across renders', () => {
+ const { store } = createAsyncTestStore();
+
+ const { result, rerender } = renderHook(() => useMutation(store, 'setVolume'));
+
+ const firstMutate = result.current.mutate;
+ rerender();
+
+ expect(result.current.mutate).toBe(firstMutate);
+ });
+
+ it('works with synchronous requests', async () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useMutation(store, 'setVolume'));
+
+ await act(async () => {
+ await result.current.mutate(0.5);
+ });
+
+ expect(result.current.status).toBe('success');
+ if (result.current.status === 'success') {
+ expect(result.current.data).toBe(0.5);
+ }
+ });
+});
diff --git a/packages/store/src/react/hooks/tests/use-request.test.tsx b/packages/store/src/react/hooks/tests/use-request.test.tsx
new file mode 100644
index 00000000..1e075497
--- /dev/null
+++ b/packages/store/src/react/hooks/tests/use-request.test.tsx
@@ -0,0 +1,37 @@
+import { renderHook } from '@testing-library/react';
+
+import { describe, expect, it } from 'vitest';
+
+import { useRequest } from '../use-request';
+import { createTestStore } from './test-utils';
+
+describe('useRequest', () => {
+ it('returns request map', () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useRequest(store));
+
+ expect(result.current).toHaveProperty('setVolume');
+ expect(result.current).toHaveProperty('setMuted');
+ expect(typeof result.current.setVolume).toBe('function');
+ });
+
+ it('returns request by name', () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useRequest(store, 'setVolume'));
+
+ expect(typeof result.current).toBe('function');
+ });
+
+ it('returns stable reference', () => {
+ const { store } = createTestStore();
+
+ const { result, rerender } = renderHook(() => useRequest(store));
+
+ const firstRequest = result.current;
+ rerender();
+
+ expect(result.current).toBe(firstRequest);
+ });
+});
diff --git a/packages/store/src/react/hooks/tests/use-selector.test.tsx b/packages/store/src/react/hooks/tests/use-selector.test.tsx
new file mode 100644
index 00000000..219bdc1a
--- /dev/null
+++ b/packages/store/src/react/hooks/tests/use-selector.test.tsx
@@ -0,0 +1,51 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { useSelector } from '../use-selector';
+import { createTestStore } from './test-utils';
+
+describe('useSelector', () => {
+ it('returns selected state', () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useSelector(store, s => s.volume));
+
+ expect(result.current).toBe(1);
+ });
+
+ it('re-renders when selected state changes', async () => {
+ const { store, target } = createTestStore();
+
+ const { result } = renderHook(() => useSelector(store, s => s.volume));
+
+ expect(result.current).toBe(1);
+
+ await act(async () => {
+ target.volume = 0.5;
+ target.dispatchEvent(new Event('volumechange'));
+ });
+
+ expect(result.current).toBe(0.5);
+ });
+
+ it('does not re-render when unrelated state changes', async () => {
+ const { store, target } = createTestStore();
+ const renderCount = vi.fn();
+
+ renderHook(() => {
+ renderCount();
+ return useSelector(store, s => s.volume);
+ });
+
+ expect(renderCount).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ target.muted = true;
+ target.dispatchEvent(new Event('volumechange'));
+ });
+
+ // Should not re-render because volume didn't change
+ expect(renderCount).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/store/src/react/hooks/tests/use-tasks.test.tsx b/packages/store/src/react/hooks/tests/use-tasks.test.tsx
new file mode 100644
index 00000000..288b0cee
--- /dev/null
+++ b/packages/store/src/react/hooks/tests/use-tasks.test.tsx
@@ -0,0 +1,31 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { describe, expect, it } from 'vitest';
+
+import { useTasks } from '../use-tasks';
+import { createTestStore } from './test-utils';
+
+describe('useTasks', () => {
+ it('returns tasks record', () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useTasks(store));
+
+ expect(result.current).toEqual({});
+ });
+
+ it('updates when task completes', async () => {
+ const { store } = createTestStore();
+
+ const { result } = renderHook(() => useTasks(store));
+
+ expect(result.current.setVolume).toBeUndefined();
+
+ await act(async () => {
+ await store.request.setVolume(0.5);
+ });
+
+ expect(result.current.setVolume).toBeDefined();
+ expect(result.current.setVolume?.status).toBe('success');
+ });
+});
diff --git a/packages/store/src/react/hooks/use-mutation.ts b/packages/store/src/react/hooks/use-mutation.ts
new file mode 100644
index 00000000..bc47f4c5
--- /dev/null
+++ b/packages/store/src/react/hooks/use-mutation.ts
@@ -0,0 +1,98 @@
+import type { EnsureFunction } from '@videojs/utils/types';
+import type { Task } from '../../core/queue';
+import type { AnyStore, InferStoreRequests } from '../../core/store';
+import type { MutationResult } from '../../shared/types';
+
+import { useCallback, useRef, useSyncExternalStore } from 'react';
+
+/**
+ * Track a store request as a mutation with status, data, and error.
+ *
+ * Subscribes to the task queue and re-renders when the mutation's status changes.
+ *
+ * Returns a discriminated union — use `status` to narrow the type and access
+ * `data` (on success) or `error` (on failure).
+ *
+ * @param store - The store instance containing the request
+ * @param name - The request name to track (type-safe with autocomplete)
+ * @returns A discriminated union with the mutation's current state
+ *
+ * @example
+ * ```tsx
+ * function SourceSelector() {
+ * const source = useMutation(store, 'setSource');
+ *
+ * return (
+ * <>
+ *
+ * {source.status === 'error' && (
+ * Failed to load: {String(source.error)}
+ * )}
+ * >
+ * );
+ * }
+ * ```
+ */
+export function useMutation<
+ Store extends AnyStore,
+ Name extends keyof InferStoreRequests,
+ Mutate extends InferStoreRequests[Name] = InferStoreRequests[Name],
+>(store: Store, name: Name): MutationResult>>> {
+ type Data = Awaited>>;
+
+ const taskRef = useRef(store.queue.tasks[name]);
+
+ const subscribe = useCallback(
+ (onStoreChange: () => void) =>
+ store.queue.subscribe((tasks) => {
+ const newTask = tasks[name];
+ if (newTask !== taskRef.current) {
+ taskRef.current = newTask;
+ onStoreChange();
+ }
+ }),
+ [store, name],
+ );
+
+ const getSnapshot = useCallback(() => taskRef.current, []);
+
+ const task = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+
+ const resetRef = useRef(() => store.queue.reset(name));
+
+ const base = {
+ mutate: store.request[name] as Mutate,
+ reset: resetRef.current,
+ };
+
+ if (task?.status === 'success') {
+ return {
+ status: 'success',
+ ...base,
+ data: task.output as Data,
+ };
+ }
+
+ if (task?.status === 'error') {
+ return {
+ status: 'error',
+ ...base,
+ error: task.error,
+ };
+ }
+
+ return {
+ status: task?.status ?? 'idle',
+ ...base,
+ } as MutationResult;
+}
+
+export namespace useMutation {
+ export type Result = MutationResult;
+}
diff --git a/packages/store/src/react/hooks/use-request.ts b/packages/store/src/react/hooks/use-request.ts
new file mode 100644
index 00000000..0041e074
--- /dev/null
+++ b/packages/store/src/react/hooks/use-request.ts
@@ -0,0 +1,44 @@
+import type { AnyStore, InferStoreRequests } from '../../core/store';
+
+import { isUndefined } from '@videojs/utils/predicate';
+
+/**
+ * Access the store's request methods.
+ *
+ * Returns either the full request map or a specific request function by name.
+ *
+ * The request map is stable across renders (same reference).
+ *
+ * @example
+ * ```tsx
+ * // Get all requests
+ * function Controls() {
+ * const request = useRequest(store);
+ * return ;
+ * }
+ *
+ * // Get a specific request by name
+ * function PlayButton() {
+ * const play = useRequest(store, 'play');
+ * return ;
+ * }
+ * ```
+ */
+export function useRequest(store: S): InferStoreRequests;
+export function useRequest>(
+ store: S,
+ name: Name,
+): InferStoreRequests[Name];
+// eslint-disable-next-line react/no-unnecessary-use-prefix
+export function useRequest>(
+ store: S,
+ name?: Name,
+): InferStoreRequests | InferStoreRequests[Name] {
+ const request = store.request as InferStoreRequests;
+
+ if (isUndefined(name)) {
+ return request;
+ }
+
+ return request[name];
+}
diff --git a/packages/store/src/react/hooks/use-selector.ts b/packages/store/src/react/hooks/use-selector.ts
new file mode 100644
index 00000000..2f95230b
--- /dev/null
+++ b/packages/store/src/react/hooks/use-selector.ts
@@ -0,0 +1,34 @@
+import type { AnyStore, InferStoreState } from '../../core/store';
+
+import { useCallback, useSyncExternalStore } from 'react';
+
+/**
+ * Subscribe to a slice of store state.
+ *
+ * Only re-renders when the selected value changes (shallow comparison).
+ *
+ * The selector function should return a stable reference for objects
+ * to avoid unnecessary re-renders.
+ *
+ * @param store - The store instance to subscribe to
+ * @param selector - Function that extracts the desired value from state
+ * @returns The selected value, updated when it changes
+ *
+ * @example
+ * ```tsx
+ * function VolumeDisplay() {
+ * const volume = useSelector(store, (s) => s.volume);
+ * return {Math.round(volume * 100)}%;
+ * }
+ * ```
+ */
+export function useSelector(store: S, selector: (state: InferStoreState) => T): T {
+ const subscribe = useCallback(
+ (onStoreChange: () => void) => store.subscribe(selector, onStoreChange),
+ [store, selector],
+ );
+
+ const getSnapshot = useCallback(() => selector(store.state), [store, selector]);
+
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+}
diff --git a/packages/store/src/react/hooks/use-tasks.ts b/packages/store/src/react/hooks/use-tasks.ts
new file mode 100644
index 00000000..9cab4ddc
--- /dev/null
+++ b/packages/store/src/react/hooks/use-tasks.ts
@@ -0,0 +1,50 @@
+import type { TasksRecord } from '../../core/queue';
+import type { AnyStore, InferStoreTasks } from '../../core/store';
+
+import { useCallback, useRef, useSyncExternalStore } from 'react';
+
+/**
+ * Subscribe to task queue state.
+ *
+ * Returns a record of all tasks keyed by request name.
+ * Re-renders when any task is added, updated, or removed.
+ *
+ * For tracking a single mutation, prefer `useMutation` which provides a more ergonomic API with
+ * status helpers.
+ *
+ * @param store - The store instance to subscribe to
+ * @returns Record of tasks keyed by request name
+ *
+ * @example
+ * ```tsx
+ * function TaskList() {
+ * const tasks = useTasks(store);
+ *
+ * return (
+ *
+ * {Object.entries(tasks).map(([name, task]) => (
+ * -
+ * {name}: {task.status}
+ *
+ * ))}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useTasks(store: S): TasksRecord> {
+ const tasksRef = useRef(store.queue.tasks);
+
+ const subscribe = useCallback(
+ (onStoreChange: () => void) =>
+ store.queue.subscribe((tasks) => {
+ tasksRef.current = tasks;
+ onStoreChange();
+ }),
+ [store],
+ );
+
+ const getSnapshot = useCallback(() => tasksRef.current as TasksRecord>, []);
+
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+}
diff --git a/packages/store/src/react/index.ts b/packages/store/src/react/index.ts
index a2bc28e5..368adfa5 100644
--- a/packages/store/src/react/index.ts
+++ b/packages/store/src/react/index.ts
@@ -3,4 +3,4 @@ export { useStoreContext } from './context';
export { createStore } from './create-store';
export type { CreateStoreConfig, CreateStoreResult, ProviderProps } from './create-store';
-export { useRequest, useSelector, useTasks } from './hooks';
+export { useMutation, useRequest, useSelector, useTasks } from './hooks';
diff --git a/packages/store/src/react/tests/create-store.test.tsx b/packages/store/src/react/tests/create-store.test.tsx
index 2a7fa18b..736e33e2 100644
--- a/packages/store/src/react/tests/create-store.test.tsx
+++ b/packages/store/src/react/tests/create-store.test.tsx
@@ -190,11 +190,11 @@ describe('createStore', () => {
expect(result.current).toHaveProperty('setVolume');
});
- it('returns selected request', () => {
+ it('returns request by name', () => {
const { Provider, useRequest, create } = createStore({ slices: [audioSlice] });
const store = create();
- const { result } = renderHook(() => useRequest(r => r.setVolume), {
+ const { result } = renderHook(() => useRequest('setVolume'), {
wrapper: ({ children }: { children: ReactNode }) => {children},
});
diff --git a/packages/store/src/react/tests/hooks.test.tsx b/packages/store/src/react/tests/hooks.test.tsx
deleted file mode 100644
index fe5964d9..00000000
--- a/packages/store/src/react/tests/hooks.test.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { act, renderHook } from '@testing-library/react';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { createSlice } from '../../core/slice';
-import { createStore as createCoreStore } from '../../core/store';
-import { useRequest, useSelector, useTasks } from '../hooks';
-
-describe('react hooks', () => {
- // Mock target
- class MockMedia extends EventTarget {
- volume = 1;
- muted = false;
- }
-
- const audioSlice = createSlice()({
- initialState: { volume: 1, muted: false },
- getSnapshot: ({ target }) => ({
- volume: target.volume,
- muted: target.muted,
- }),
- subscribe: ({ target, update, signal }) => {
- target.addEventListener('volumechange', update);
- signal.addEventListener('abort', () => {
- target.removeEventListener('volumechange', update);
- });
- },
- request: {
- setVolume: (volume: number, { target }) => {
- target.volume = volume;
- target.dispatchEvent(new Event('volumechange'));
- return volume;
- },
- setMuted: (muted: boolean, { target }) => {
- target.muted = muted;
- target.dispatchEvent(new Event('volumechange'));
- return muted;
- },
- },
- });
-
- function createTestStore() {
- const store = createCoreStore({ slices: [audioSlice] });
- const target = new MockMedia();
- store.attach(target);
- return { store, target };
- }
-
- describe('useSelector', () => {
- it('returns selected state', () => {
- const { store } = createTestStore();
-
- const { result } = renderHook(() => useSelector(store, s => s.volume));
-
- expect(result.current).toBe(1);
- });
-
- it('re-renders when selected state changes', async () => {
- const { store, target } = createTestStore();
-
- const { result } = renderHook(() => useSelector(store, s => s.volume));
-
- expect(result.current).toBe(1);
-
- await act(async () => {
- target.volume = 0.5;
- target.dispatchEvent(new Event('volumechange'));
- });
-
- expect(result.current).toBe(0.5);
- });
-
- it('does not re-render when unrelated state changes', async () => {
- const { store, target } = createTestStore();
- const renderCount = vi.fn();
-
- renderHook(() => {
- renderCount();
- return useSelector(store, s => s.volume);
- });
-
- expect(renderCount).toHaveBeenCalledTimes(1);
-
- await act(async () => {
- target.muted = true;
- target.dispatchEvent(new Event('volumechange'));
- });
-
- // Should not re-render because volume didn't change
- expect(renderCount).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('useRequest', () => {
- it('returns request map', () => {
- const { store } = createTestStore();
-
- const { result } = renderHook(() => useRequest(store));
-
- expect(result.current).toHaveProperty('setVolume');
- expect(result.current).toHaveProperty('setMuted');
- expect(typeof result.current.setVolume).toBe('function');
- });
-
- it('returns selected request', () => {
- const { store } = createTestStore();
-
- const { result } = renderHook(() => useRequest(store, r => r.setVolume));
-
- expect(typeof result.current).toBe('function');
- });
-
- it('returns stable reference', () => {
- const { store } = createTestStore();
-
- const { result, rerender } = renderHook(() => useRequest(store));
-
- const firstRequest = result.current;
- rerender();
-
- expect(result.current).toBe(firstRequest);
- });
- });
-
- describe('useTasks', () => {
- it('returns tasks record', () => {
- const { store } = createTestStore();
-
- const { result } = renderHook(() => useTasks(store));
-
- expect(result.current).toEqual({});
- });
-
- it('updates when task completes', async () => {
- const { store } = createTestStore();
-
- const { result } = renderHook(() => useTasks(store));
-
- expect(result.current.setVolume).toBeUndefined();
-
- await act(async () => {
- await store.request.setVolume(0.5);
- });
-
- expect(result.current.setVolume).toBeDefined();
- expect(result.current.setVolume?.status).toBe('success');
- });
- });
-});
diff --git a/packages/store/src/shared/types.ts b/packages/store/src/shared/types.ts
new file mode 100644
index 00000000..ebda14aa
--- /dev/null
+++ b/packages/store/src/shared/types.ts
@@ -0,0 +1,144 @@
+// ----------------------------------------
+// Async Status
+// ----------------------------------------
+
+/**
+ * Lifecycle status for async operations.
+ *
+ * - `'idle'` — Operation hasn't started yet
+ * - `'pending'` — Operation is in flight
+ * - `'success'` — Operation completed successfully
+ * - `'error'` — Operation failed with an error
+ */
+export type AsyncStatus = 'idle' | 'pending' | 'success' | 'error';
+
+// ----------------------------------------
+// Mutation Types
+// ----------------------------------------
+
+/**
+ * Common properties shared by all mutation states.
+ */
+interface MutationBase {
+ status: AsyncStatus;
+ mutate: Mutate;
+ reset: () => void;
+}
+
+/**
+ * Mutation hasn't been triggered yet.
+ * This is the initial state before calling `mutate()`.
+ */
+export interface MutationIdle extends MutationBase {
+ status: 'idle';
+}
+
+/**
+ * Mutation is in flight, waiting for the request to complete.
+ * The UI should typically show a loading indicator.
+ */
+export interface MutationPending extends MutationBase {
+ status: 'pending';
+}
+
+/**
+ * Mutation completed successfully.
+ * The `data` property contains the request's return value.
+ */
+export interface MutationSuccess extends MutationBase {
+ status: 'success';
+ data: Data;
+}
+
+/**
+ * Mutation failed with an error.
+ * The `error` property contains the thrown exception.
+ */
+export interface MutationError extends MutationBase {
+ status: 'error';
+ error: unknown;
+}
+
+/**
+ * Discriminated union representing all possible mutation states.
+ *
+ * Use `status` to narrow the type and access state-specific properties:
+ *
+ * ```ts
+ * if (mutation.status === 'success') {
+ * console.log(mutation.data); // Data is available
+ * }
+ * if (mutation.status === 'error') {
+ * console.log(mutation.error); // Error is available
+ * }
+ * ```
+ */
+export type MutationResult
+ = | MutationIdle
+ | MutationPending
+ | MutationSuccess
+ | MutationError;
+
+// ----------------------------------------
+// Optimistic Types
+// ----------------------------------------
+
+/**
+ * Common properties shared by all optimistic states.
+ */
+interface OptimisticBase {
+ value: Value;
+ setValue: SetValue;
+ reset: () => void;
+}
+
+/**
+ * No optimistic update is active.
+ * The `value` reflects the actual store state.
+ */
+export interface OptimisticIdle extends OptimisticBase {
+ status: 'idle';
+}
+
+/**
+ * An optimistic update is in flight.
+ * The `value` shows the optimistic (predicted) value while waiting.
+ */
+export interface OptimisticPending extends OptimisticBase {
+ status: 'pending';
+}
+
+/**
+ * The optimistic update completed successfully.
+ * The `value` now reflects the confirmed store state.
+ */
+export interface OptimisticSuccess extends OptimisticBase {
+ status: 'success';
+}
+
+/**
+ * The optimistic update failed.
+ * The `value` has reverted to the actual store state.
+ * The `error` property contains the thrown exception.
+ */
+export interface OptimisticError extends OptimisticBase {
+ status: 'error';
+ error: unknown;
+}
+
+/**
+ * Discriminated union representing all possible optimistic update states.
+ *
+ * Use `status` to narrow the type and access state-specific properties:
+ *
+ * ```ts
+ * if (optimistic.status === 'error') {
+ * console.log(optimistic.error); // Error is available
+ * }
+ * ```
+ */
+export type OptimisticResult
+ = | OptimisticIdle
+ | OptimisticPending
+ | OptimisticSuccess
+ | OptimisticError;
diff --git a/packages/store/tsconfig.json b/packages/store/tsconfig.json
index 35146f7c..af8bdd14 100644
--- a/packages/store/tsconfig.json
+++ b/packages/store/tsconfig.json
@@ -5,5 +5,5 @@
"declarationDir": "types"
},
"references": [{ "path": "../utils" }],
- "include": ["src/core/**/*.ts"]
+ "include": ["src/core", "src/shared"]
}