createAsyncThunk и RTK Query
createAsyncThunk
Заголовок раздела «createAsyncThunk»import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
interface User { id: number; name: string; email: string;}
// Создаём async thunk// Типы: createAsyncThunk<ReturnType, ArgType, ThunkAPI>export const fetchUser = createAsyncThunk<User, number>( 'users/fetchUser', async (userId, thunkAPI) => { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { return thunkAPI.rejectWithValue(`HTTP ${response.status}`); } return response.json() as Promise<User>; });
export const createUser = createAsyncThunk<User, Omit<User, 'id'>>( 'users/createUser', async (userData, { rejectWithValue }) => { try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise<User>; } catch (err) { return rejectWithValue(err instanceof Error ? err.message : 'Unknown error'); } });Обработка состояний в slice
Заголовок раздела «Обработка состояний в slice»interface UsersState { users: User[]; currentUser: User | null; loading: { fetchUser: boolean; createUser: boolean; }; errors: { fetchUser: string | null; createUser: string | null; };}
const initialState: UsersState = { users: [], currentUser: null, loading: { fetchUser: false, createUser: false }, errors: { fetchUser: null, createUser: null },};
const usersSlice = createSlice({ name: 'users', initialState, reducers: { clearCurrentUser(state) { state.currentUser = null; }, }, // extraReducers — для обработки async thunk actions extraReducers: builder => { // fetchUser builder .addCase(fetchUser.pending, state => { state.loading.fetchUser = true; state.errors.fetchUser = null; }) .addCase(fetchUser.fulfilled, (state, action: PayloadAction<User>) => { state.loading.fetchUser = false; state.currentUser = action.payload; // Добавить в список если ещё нет if (!state.users.find(u => u.id === action.payload.id)) { state.users.push(action.payload); } }) .addCase(fetchUser.rejected, (state, action) => { state.loading.fetchUser = false; state.errors.fetchUser = action.payload as string ?? action.error.message ?? 'Error'; });
// createUser builder .addCase(createUser.pending, state => { state.loading.createUser = true; state.errors.createUser = null; }) .addCase(createUser.fulfilled, (state, action: PayloadAction<User>) => { state.loading.createUser = false; state.users.push(action.payload); }) .addCase(createUser.rejected, (state, action) => { state.loading.createUser = false; state.errors.createUser = action.payload as string ?? 'Failed to create user'; }); },});
export const { clearCurrentUser } = usersSlice.actions;export default usersSlice.reducer;Использование в компонентах
Заголовок раздела «Использование в компонентах»import { useEffect } from 'react';import { useAppDispatch, useAppSelector } from '../../store/hooks';import { fetchUser, createUser } from './usersSlice';
function UserProfile({ userId }: { userId: number }) { const dispatch = useAppDispatch(); const { currentUser, loading, errors } = useAppSelector(state => state.users);
useEffect(() => { dispatch(fetchUser(userId)); }, [dispatch, userId]);
if (loading.fetchUser) return <Spinner />; if (errors.fetchUser) return <ErrorMessage message={errors.fetchUser} />; if (!currentUser) return null;
return <div>{currentUser.name}</div>;}
function CreateUserForm() { const dispatch = useAppDispatch(); const { loading, errors } = useAppSelector(state => state.users);
async function handleSubmit(data: Omit<User, 'id'>) { const result = await dispatch(createUser(data));
if (createUser.fulfilled.match(result)) { // Успех console.log('Created:', result.payload); } else { // Ошибка console.error('Failed:', result.payload); } }
return ( <form onSubmit={/* ... */}> <button disabled={loading.createUser}> {loading.createUser ? 'Создание...' : 'Создать'} </button> {errors.createUser && <span>{errors.createUser}</span>} </form> );}Отмена запросов
Заголовок раздела «Отмена запросов»export const fetchSearchResults = createAsyncThunk< string[], string, { signal: AbortSignal }>( 'search/fetch', async (query, { signal }) => { const response = await fetch(`/api/search?q=${query}`, { signal }); return response.json(); });
// В компоненте:function SearchBox() { const dispatch = useAppDispatch();
useEffect(() => { const promise = dispatch(fetchSearchResults(query)); return () => promise.abort(); // отменяем при изменении query или размонтировании }, [dispatch, query]);}RTK Query — basics
Заголовок раздела «RTK Query — basics»RTK Query — встроенный в RTK инструмент для data fetching с кэшированием:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), tagTypes: ['User', 'Post'], endpoints: builder => ({ // Query (GET) getUsers: builder.query<User[], void>({ query: () => '/users', providesTags: result => result ? [...result.map(({ id }) => ({ type: 'User' as const, id })), 'User'] : ['User'], }), getUserById: builder.query<User, number>({ query: id => `/users/${id}`, providesTags: (_, __, id) => [{ type: 'User', id }], }), // Mutation (POST/PUT/DELETE) createUser: builder.mutation<User, Omit<User, 'id'>>({ query: body => ({ url: '/users', method: 'POST', body }), invalidatesTags: ['User'], // автоматически инвалидирует кэш }), updateUser: builder.mutation<User, Partial<User> & { id: number }>({ query: ({ id, ...body }) => ({ url: `/users/${id}`, method: 'PATCH', body }), invalidatesTags: (_, __, { id }) => [{ type: 'User', id }], }), }),});
export const { useGetUsersQuery, useGetUserByIdQuery, useCreateUserMutation, useUpdateUserMutation,} = api;
// Добавить в configureStore:const store = configureStore({ reducer: { [api.reducerPath]: api.reducer, // ... }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(api.middleware),});Использование RTK Query хуков
Заголовок раздела «Использование RTK Query хуков»function UsersList() { const { data: users, isLoading, error } = useGetUsersQuery();
if (isLoading) return <Spinner />; if (error) return <ErrorMessage />;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}
function UserDetail({ id }: { id: number }) { const { data: user, isFetching } = useGetUserByIdQuery(id, { skip: !id, // не запрашивать если id нет pollingInterval: 30000, // обновлять каждые 30 секунд });
return isFetching ? <Spinner /> : <div>{user?.name}</div>;}
function CreateUserButton() { const [createUser, { isLoading }] = useCreateUserMutation();
async function handleCreate() { try { const newUser = await createUser({ name: 'Новый', email: 'new@test.ru' }).unwrap(); console.log('Created:', newUser); } catch (err) { console.error('Failed:', err); } }
return ( <button onClick={handleCreate} disabled={isLoading}> {isLoading ? 'Создание...' : 'Создать'} </button> );}