Сегодня нас ждет самый увлекательный и комплексный урок. Мы не будем изучать новые концепции, а соберем воедино все, что прошли за эти 19 занятий и создадим с вами полноценное мини-приложение, это интерфейс для блога.
20-й урок является последним. Он позволит вам применить на практике все полученные навыки, от создания слайсов и работы с асинхронными запросами до структурирования проекта и использования TypeScript. Если вы где-то запнетесь, не беда. Это абсолютно нормально. Возвращайтесь к предыдущим урокам, освежайте знания. Цель этого занятия не просто скопировать код, а понять, как все части пазла под названием «Redux Toolkit» складываются в единую картину.
Архитектура нашего блога
Прежде чем писать код, давайте продумаем, что мы хотим получить в итоге. Наше приложение будет отображать список постов, для каждого поста показывать автора (пользователя), а также позволять просматривать и добавлять комментарии. Это классическая структура, которая встречается в бесчисленном количестве веб-приложений.
Мы будем использовать публичное API JSONPlaceholder. Он идеально подходит для таких учебных проектов. От него мы получим три основные сущности:
-
Посты (
/posts). Заголовок, тело поста и ID пользователя. -
Пользователи (
/users). Имя, username и другие данные. -
Комментарии (
/comments). Текст комментария, email автора и ID поста, к которому он привязан.
Главная задача, не просто загрузить эти данные, а умело их связать между собой. Например, когда мы получаем пост, мы должны по userId найти соответствующего пользователя и подставить его имя. А при отображении поста, фильтровать комментарии по postId. Это та самая «комбинация состояния» из нескольких слайсов, которую мы рассматривали.
Техническое задание для самостоятельной реализации
Я предлагаю вам следующее ТЗ. Попробуйте реализовать его самостоятельно, используя наши прошлые уроки как шпаргалку. Ниже я дам подсказки и примеры кода, но старайтесь сначала сделать сами.
Нужно создать SPA (Single Page Application) «Блог» с использованием React, TypeScript, Redux Toolkit и RTK Query.
Функциональные требования
-
Страница списка постов:
-
Отображает карточки всех постов.
-
В каждой карточке должен быть заголовок, краткое содержание (первые 100 символов тела поста), имя автора и количество комментариев.
-
Должна быть реализована фильтрация постов по имени пользователя (выпадающий список с пользователями).
-
-
Страница отдельного поста (при клике на карточку):
-
Отображает полный текст поста, имя автора и список всех комментариев к этому посту.
-
Под списком комментариев должна быть форма для добавления нового комментария (поля:
name,email,body). Так как JSONPlaceholder не сохраняет данные по-настоящему, после отправки форма просто очищается, а новый комментарий добавляется в список локально (в стейт Redux).
-
-
Общее:
-
Реализовать индикаторы загрузки и обработку ошибок при запросах.
-
Все асинхронные операции (получение постов, пользователей, комментариев) должны выполняться через RTK Query.
-
Структура проекта
Для такого проекта организация кода, это половина успеха. Давайте сразу делать это правильно, по принципу feature-based folders (функциональные папки). Это означает, что мы группируем всю логику (слайсы, компоненты, стили, типы) вокруг фич, а не типов файлов.
Вот как может выглядеть наша структура папок в src:
src/ │ ├── app/ │ ├── store.ts # Централизованная настройка store │ └── hooks.ts # Типизированные хуки useDispatch и useSelector │ ├── features/ │ ├── api/ │ │ └── blogApi.ts # Основной API slice, созданный с помощью createApi │ │ │ ├── posts/ │ │ ├── PostsList.tsx │ │ ├── PostCard.tsx │ │ ├── PostDetails.tsx │ │ └── postsSlice.ts # (Опционально) для локального состояния, например, фильтров │ │ │ ├── users/ │ │ ├── UserSelect.tsx │ │ └── usersSlice.ts # (Опционально) │ │ │ └── comments/ │ ├── CommentsList.tsx │ ├── AddCommentForm.tsx │ └── commentsSlice.ts # Для локального управления добавленными комментариями │ ├── shared/ │ └── types.ts # Общие типы TypeScript для всего приложения │ └── main.tsx
Почему так удобно? Представьте, что вам нужно изменить логику работы с комментариями. Вместо того чтобы бегать по разным папкам (/slices, /components, /pages), вы открываете одну папку features/comments и находите там все необходимое.
RTK Query как стержень данных. Обратите внимание на папку features/api/blogApi.ts. Вся работа с бэкендом будет сосредоточена здесь. Мы создадим один большой createApi эндпоинт, который будет описывать все наши запросы к JSONPlaceholder.
Пишем код вместе
Давайте набросаем ключевые части приложения. Я покажу примеры, которые помогут вам стартовать.
1. Настраиваем Store и основной API
Сначала создадим наш store и основной API-слайс.
app/store.ts
import { configureStore } from '@reduxjs/toolkit'; import { blogApi } from '../features/api/blogApi'; import commentsReducer from '../features/comments/commentsSlice'; // Импортируем редьюсер для локальных комментариев export const store = configureStore({ reducer: { // Добавляем редьюсер из RTK Query [blogApi.reducerPath]: blogApi.reducer, // Наш собственный редьюсер для локальных комментариев comments: commentsReducer, }, // Подключаем middleware для кэширования и инвалидации middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(blogApi.middleware), }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch;
features/api/blogApi.ts
Это сердце нашего приложения. Здесь мы описываем все данные, которые будем получать.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; // Определяем типы для наших сущностей export interface Post { id: number; title: string; body: string; userId: number; } export interface User { id: number; name: string; username: string; email: string; } export interface Comment { id: number; postId: number; name: string; email: string; body: string; } export const blogApi = createApi({ reducerPath: 'blogApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/', }), tagTypes: ['Post', 'User', 'Comment'], // Теги для инвалидации кэша endpoints: (builder) => ({ // Получение всех постов getPosts: builder.query<Post[], void>({ query: () => '/posts', providesTags: (result) => // Предоставляем теги для полученных постов result ? [ ...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }, ] : [{ type: 'Post', id: 'LIST' }], }), // Получение поста по ID getPostById: builder.query<Post, number>({ query: (id) => `/posts/${id}`, providesTags: (result, error, id) => [{ type: 'Post', id }], }), // Получение всех пользователей getUsers: builder.query<User[], void>({ query: () => '/users', providesTags: ['User'], }), // Получение пользователя по ID getUserById: builder.query<User, number>({ query: (id) => `/users/${id}`, providesTags: (result, error, id) => [{ type: 'User', id }], }), // Получение комментариев для конкретного поста getCommentsByPostId: builder.query<Comment[], number>({ query: (postId) => `/comments?postId=${postId}`, providesTags: (result, error, postId) => [{ type: 'Comment', postId }], }), // Добавление нового комментария (будет работать "вхолостую") addComment: builder.mutation<Comment, Partial<Comment>>({ query: (newComment) => ({ url: '/comments', method: 'POST', body: newComment, }), // Инвалидируем кэш комментариев для конкретного поста, чтобы теоретически перезапросить их invalidatesTags: (result, error, { postId }) => [ { type: 'Comment', postId }, ], }), }), }); // Экспортируем автоматически сгенерированные хуки export const { useGetPostsQuery, useGetPostByIdQuery, useGetUsersQuery, useGetUserByIdQuery, useGetCommentsByPostIdQuery, useAddCommentMutation, } = blogApi;
2. Локальное состояние для комментариев
Поскольку JSONPlaceholder не сохраняет наши новые комментарии, мы будем хранить их в своем слайсе. Это покажет, как совмещать RTK Query и «классические» слайсы.
features/comments/commentsSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Comment } from '../api/blogApi'; interface CommentsState { localComments: Comment[]; // Массив для хранения добавленных нами комментариев } const initialState: CommentsState = { localComments: [], }; const commentsSlice = createSlice({ name: 'comments', initialState, reducers: { // Редьюсер для добавления нового комментария в локальный массив addLocalComment: (state, action: PayloadAction<Comment>) => { // Генерируем временный ID, чтобы не было конфликтов с серверными const tempId = Math.random() * 1000000; state.localComments.push({ ...action.payload, id: tempId }); }, }, }); export const { addLocalComment } = commentsSlice.actions; export default commentsSlice.reducer;
3. Компонент списка постов с фильтрацией
Теперь создадим главную страницу. Мы будем использовать хуки из blogApi и useSelector для доступа к локальным комментариям.
features/posts/PostsList.tsx
import React, { useState, useMemo } from 'react'; import { useGetPostsQuery, useGetUsersQuery, Post } from '../api/blogApi'; import { useAppSelector } from '../../../app/hooks'; import PostCard from './PostCard'; import UserSelect from '../../users/UserSelect'; const PostsList: React.FC = () => { const { data: posts, isLoading, error } = useGetPostsQuery(); const { data: users } = useGetUsersQuery(); const [selectedUserId, setSelectedUserId] = useState<number | null>(null); // Получаем локальные комментарии из store const localComments = useAppSelector((state) => state.comments.localComments); // Мемоизированный вычисленный список постов с фильтрацией const filteredPosts = useMemo(() => { if (!posts) return []; let filtered = posts; if (selectedUserId) { filtered = posts.filter((post) => post.userId === selectedUserId); } // Обогащаем посты информацией о количестве комментариев return filtered.map((post) => { const serverCommentsCount = 5; // JSONPlaceholder всегда возвращает 5 комментов для поста. В реальном API мы бы использовали useGetCommentsByPostIdQuery. const localCommentsForPost = localComments.filter((comment) => comment.postId === post.id); const totalCommentsCount = serverCommentsCount + localCommentsForPost.length; return { ...post, commentCount: totalCommentsCount, }; }); }, [posts, selectedUserId, localComments]); if (isLoading) return <div>Загрузка постов...</div>; if (error) return <div>Ошибка при загрузке постов!</div>; return ( <div> <h1>Все посты</h1> {/* Компонент выбора пользователя для фильтрации */} <UserSelect users={users || []} selectedUserId={selectedUserId} onSelectUser={(id) => setSelectedUserId(id)} /> <div className="posts-grid"> {filteredPosts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> </div> ); }; export default PostsList;
Этот компонент демонстрирует всю суть селекторов и мемоизации. Мы динамически фильтруем и обогащаем данные, полученные из API, данными из нашего локального состояния.
Итоги все курса по Redux и Redux Toolkit
Мы начали с простого useState и проблемы прокидывания пропсов, изучили базовые концепции Redux, а затем открыли для себя Redux Toolkit. Мы научились создавать слайсы, работать с асинхронностью через createAsyncThunk и освоили вершину удобства, это RTK Query для бесшовной работы с API.
Главные преимущества Redux Toolkit, которые вы теперь ощутили на себе:
-
Меньше шаблонного кода.
createSliceсам генерирует экшены и редьюсеры. -
Встроенные лучшие практики. Immer для иммутабельных обновлений,
configureStoreс подключенными DevTools по умолчанию. -
Удобство RTK Query. Интуитивное управление кэшированием, инвалидацией и сетевыми запросами, которое практически не требует написания кода.
-
Предсказуемость и отладка. Redux DevTools остаются вашим верным союзником, позволяя отслеживать каждое изменение состояния и «путешествовать во времени».
Курс подошел к концу, но ваше обучение не заканчивается. Теперь вы можете уверенно работать с состоянием в любом React-приложении.
Куда двигаться дальше?
-
redux-persist. Эта библиотека позволяет сохранять состояние вашего Redux-стора вlocalStorage(или другом хранилище). После перезагрузки страницы приложение «вспомнит» все данные (например, корзину покупок, токен авторизации или, как в нашем случае, локально добавленные комментарии). Это следующий логичный шаг для улучшения UX. -
Изучение соседних технологий. Углубитесь в TypeScript, изучите современные фреймворки, такие как Next.js, который отлично дружит с RTK Query.
-
Оптимизация производительности. Изучите, как работают мемоизированные селекторы с
createSelector, чтобы предотвратить лишние перерисовки компонентов. -
Возьмите другой публичный API и создайте свой проект. Например интерфейс для погодного приложения, каталог фильмов или небольшой интернет-магазин.
Я надеюсь, что этот курс был для вас полезным и интересным. Не бойтесь ошибаться. Каждая написанная вами строчка кода, это шаг к становлению вас как профессионала.
Полный курс с уроками по Redux и Redux Toolkit для начинающих.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


