From harness-claude
Implements Redux async thunks via createAsyncThunk for API data fetching and operations with loading/error state tracking. Ideal when RTK Query is overkill for simple fetches.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Handle async operations with createAsyncThunk for structured pending/fulfilled/rejected lifecycle management
Types Redux Toolkit state, actions, thunks, hooks, and middleware with full TypeScript inference and minimal annotations. Use for setup, fixing type errors in slices, extraReducers, or components.
Guides Redux and Redux Toolkit for global state management: slices, stores, actions, reducers, hooks, selectors, middleware, async thunks.
Provides fp-ts patterns for React: Option for optional state, Either for form validation, TaskEither for async data fetching, RemoteData for loading/error states. For React 18/19, Next.js 14/15.
Share bugs, ideas, or general feedback.
Handle async operations with createAsyncThunk for structured pending/fulfilled/rejected lifecycle management
createAsyncThunk using a descriptive action type prefix: '<slice>/<operation>'.dispatch(thunk(arg)), and thunkAPI which provides dispatch, getState, rejectWithValue, and signal.rejectWithValue for known error shapes — it gives the reducer a typed payload instead of a serialized error.pending, fulfilled, rejected) in the slice's extraReducers.'idle' | 'loading' | 'succeeded' | 'failed') rather than separate booleans.condition to prevent duplicate fetches — return false to skip execution.thunkAPI.signal to support cancellation via AbortController.// features/users/users.thunks.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '../../store';
interface User {
id: string;
name: string;
email: string;
}
export const fetchUsers = createAsyncThunk<
User[], // Return type
void, // Argument type
{ state: RootState; rejectValue: string }
>(
'users/fetchAll',
async (_, { rejectWithValue, signal }) => {
const response = await fetch('/api/users', { signal });
if (!response.ok) {
return rejectWithValue(`Failed: ${response.status}`);
}
return response.json();
},
{
condition: (_, { getState }) => {
const { status } = getState().users;
// Don't fetch if already loading or loaded
return status === 'idle' || status === 'failed';
},
}
);
// features/users/users.slice.ts — extraReducers
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ?? action.error.message ?? 'Unknown error';
});
},
Thunk lifecycle: Dispatching a thunk returns a promise. The thunk dispatches pending immediately, then fulfilled or rejected when the async work completes. The returned promise resolves with the action object in all cases (even rejection).
Unwrapping results: Use .unwrap() to get the payload directly or throw on rejection — useful in component handlers:
try {
const users = await dispatch(fetchUsers()).unwrap();
showSuccess(`Loaded ${users.length} users`);
} catch (err) {
showError(err as string);
}
Cancellation: When the component unmounts, abort the thunk:
useEffect(() => {
const promise = dispatch(fetchUsers());
return () => promise.abort();
}, [dispatch]);
When to use RTK Query instead: If you have a REST/GraphQL API with standard CRUD, caching, polling, or optimistic updates, RTK Query handles all of this automatically. Use createAsyncThunk for non-standard async work (WebSocket messages, file uploads, multi-step workflows).
https://redux-toolkit.js.org/api/createAsyncThunk