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

react-redux интеграция

main.tsx
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>
);
store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';
// Используй эти хуки везде, не базовые useDispatch/useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import { useAppSelector } from '../../store/hooks';
// Простой selector
function 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: отдельные вызовы useSelector
function 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 через reselect:

import { createSelector } from '@reduxjs/toolkit'; // reselect встроен в RTK
import type { RootState } from '../store';
// Простые input selectors
const selectUsers = (state: RootState) => state.users.users;
const selectFilter = (state: RootState) => state.users.filter;
// Мемоизированный computed selector
export const selectFilteredUsers = createSelector(
[selectUsers, selectFilter],
(users, filter) => {
if (!filter) return users;
return users.filter(u =>
u.name.toLowerCase().includes(filter.toLowerCase())
);
}
// Пересчитывается только если users или filter изменились
);
// Параметризованный selector
export 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;
}
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>
);
}
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);
});
},
});
// Встроенные selectors
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds,
selectTotal: selectPostsTotal,
} = postsAdapter.getSelectors((state: RootState) => state.posts);
store/index.ts
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 с доступом к getState
import { 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();
}
);