Перейти к содержимому

createAsyncThunk и RTK Query

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');
}
}
);
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 — встроенный в 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),
});
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>
);
}