react-redux интеграция
Подключение store
Заголовок раздела «Подключение store»import React from 'react';import ReactDOM from 'react-dom/client';import { Provider } from 'react-redux';import { store } from './store';import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>);Типизированные хуки
Заголовок раздела «Типизированные хуки»import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';import type { RootState, AppDispatch } from './index';
// Используй эти хуки везде, не базовые useDispatch/useSelectorexport const useAppDispatch = () => useDispatch<AppDispatch>();export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;useSelector
Заголовок раздела «useSelector»import { useAppSelector } from '../../store/hooks';
// Простой selectorfunction CounterDisplay() { const count = useAppSelector(state => state.counter.value); return <span>{count}</span>;}
// Выбор нескольких значенийfunction UserStatus() { const { currentUser, loading } = useAppSelector(state => ({ currentUser: state.users.currentUser, loading: state.users.loading.fetchUser, })); // Проблема: этот объект создаётся заново при каждом рендере // → перерендер даже если значения не изменились!}
// Решение 1: отдельные вызовы useSelectorfunction UserStatus2() { const currentUser = useAppSelector(state => state.users.currentUser); const loading = useAppSelector(state => state.users.loading.fetchUser); return loading ? <Spinner /> : <span>{currentUser?.name}</span>;}
// Решение 2: shallowEqual для объектовimport { shallowEqual } from 'react-redux';
function UserStatus3() { const { currentUser, loading } = useAppSelector( state => ({ currentUser: state.users.currentUser, loading: state.users.loading.fetchUser, }), shallowEqual // сравнивает поверхностно, не по ссылке ); return loading ? <Spinner /> : <span>{currentUser?.name}</span>;}Selectors с createSelector
Заголовок раздела «Selectors с createSelector»Мемоизированные selectors через reselect:
import { createSelector } from '@reduxjs/toolkit'; // reselect встроен в RTKimport type { RootState } from '../store';
// Простые input selectorsconst selectUsers = (state: RootState) => state.users.users;const selectFilter = (state: RootState) => state.users.filter;
// Мемоизированный computed selectorexport const selectFilteredUsers = createSelector( [selectUsers, selectFilter], (users, filter) => { if (!filter) return users; return users.filter(u => u.name.toLowerCase().includes(filter.toLowerCase()) ); } // Пересчитывается только если users или filter изменились);
// Параметризованный selectorexport const selectUserById = createSelector( [selectUsers, (_: RootState, id: number) => id], (users, id) => users.find(u => u.id === id) ?? null);
// Использованиеfunction FilteredList() { const users = useAppSelector(selectFilteredUsers); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}
function UserCard({ userId }: { userId: number }) { const user = useAppSelector(state => selectUserById(state, userId)); return user ? <div>{user.name}</div> : null;}useDispatch
Заголовок раздела «useDispatch»import { useAppDispatch } from '../../store/hooks';import { increment, setStep } from './counterSlice';import { fetchUser } from '../users/usersSlice';
function Counter() { const dispatch = useAppDispatch(); const count = useAppSelector(state => state.counter.value);
return ( <div> <button onClick={() => dispatch(increment())}>+1</button> <span>{count}</span> <button onClick={() => dispatch(setStep(5))}>Шаг 5</button> <button onClick={() => dispatch(fetchUser(1))}>Загрузить пользователя</button> </div> );}Паттерны структуры
Заголовок раздела «Паттерны структуры»Feature-based структура (рекомендуется)
Заголовок раздела «Feature-based структура (рекомендуется)»src/ store/ index.ts — configureStore hooks.ts — useAppDispatch, useAppSelector features/ auth/ authSlice.ts — createSlice + extraReducers authThunks.ts — createAsyncThunk authSelectors.ts — createSelector AuthForm.tsx — компонент index.ts — re-exports products/ productsSlice.ts productsApi.ts — RTK Query api ProductList.tsx types/ index.ts — общие типыНормализация данных
Заголовок раздела «Нормализация данных»import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
interface Post { id: number; title: string; userId: number;}
// EntityAdapter — встроенная нормализацияconst postsAdapter = createEntityAdapter<Post>({ sortComparer: (a, b) => a.title.localeCompare(b.title),});
const postsSlice = createSlice({ name: 'posts', initialState: postsAdapter.getInitialState({ loading: false, error: null as string | null, }), reducers: { postAdded: postsAdapter.addOne, postsReceived: postsAdapter.setAll, postUpdated: postsAdapter.updateOne, postDeleted: postsAdapter.removeOne, }, extraReducers: builder => { builder .addCase(fetchPosts.pending, state => { state.loading = true; }) .addCase(fetchPosts.fulfilled, (state, action) => { state.loading = false; postsAdapter.setAll(state, action.payload); }); },});
// Встроенные selectorsexport const { selectAll: selectAllPosts, selectById: selectPostById, selectIds: selectPostIds, selectTotal: selectPostsTotal,} = postsAdapter.getSelectors((state: RootState) => state.posts);TypeScript: типизация всего стека
Заголовок раздела «TypeScript: типизация всего стека»import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({ reducer: { /* ... */ } });
export type RootState = ReturnType<typeof store.getState>;export type AppDispatch = typeof store.dispatch;export default store;
// Типизированный thunk с доступом к getStateimport { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUserPosts = createAsyncThunk< Post[], // return type number, // arg type { state: RootState; dispatch: AppDispatch; rejectValue: string }>( 'posts/fetchByUser', async (userId, { getState, rejectWithValue }) => { const token = getState().auth.token; // типизированный getState! if (!token) return rejectWithValue('Not authenticated');
const response = await fetch(`/api/users/${userId}/posts`, { headers: { Authorization: `Bearer ${token}` }, }); return response.json(); });