Хотя Redux — мощный инструмент, он легко превращается в источник боли, если использовать его неправильно. Давайте разберём ошибки, которые чаще всего встречаются у разработчиков, и постараемся понять, как их избежать.
Ошибка 1: чрезмерное дублирование логики
Один из самых распространённых сценариев — дублирование кода в разных частях приложения. Например, вы пишете кучу одинаковых обработчиков состояний в нескольких слайсах или добавляете сходные функции в разных компонентах. Это не только увеличивает объём кода, но и затрудняет его поддержку.
Как избежать:
Используйте утилитарные функции и общие действия. Например, если вам нужно обнулить состояние нескольких слайсов при логауте, можно использовать extraReducers в createSlice и одно общее действие:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Общий экшен для сброса состояния
const resetState = 'app/resetState';
const userSlice = createSlice({
name: 'user',
initialState: { name: '', email: '' },
reducers: {
setUser(state, action: PayloadAction<{ name: string; email: string }>) {
state.name = action.payload.name;
state.email = action.payload.email;
},
},
extraReducers: (builder) => {
builder.addCase(resetState, () => {
return { name: '', email: '' }; // Сброс состояния
});
},
});
export const { setUser } = userSlice.actions;
// Использование в компоненте:
dispatch({ type: resetState });
Ошибка 2: хранилище перегружено данными
Некоторые разработчики склонны складывать всё в Redux. Например, данные полей формы, которые можно было бы локально хранить в useState, или большой JSON-объект с API, который используется только на одной странице. Это приводит к избыточному росту store и снижению производительности приложения.
Как избежать:
Старайтесь использовать локальное состояние (useState, useReducer в компонентах) для данных, которые не нужны в глобальном масштабе. Redux — для действительно глобального состояния, например, данных пользователя, токенов, тем приложения.
Пример: локальное состояние полей формы
const [formData, setFormData] = React.useState({ name: '', email: '' });
return (
<TextInput
value={formData.name}
onChangeText={(text) => setFormData({ ...formData, name: text })}
/>
);
Ошибка 3: неправильная работа с асинхронными действиями
При использовании createAsyncThunk можно столкнуться с несколькими проблемами: неправильная обработка ошибок, отсутствие проверок на статус загрузки, попытки выполнить новые запросы до завершения предыдущих. Это делает логику приложения хаотичной.
Как избежать:
Убедитесь, что вы учитываете все состояния загрузки (pending, fulfilled, rejected) и корректно их обрабатываете. Вот пример безопасного выполнения асинхронного действия:
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId: string, { rejectWithValue }) => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const userSlice = createSlice({
name: 'user',
initialState: { data: null, status: 'idle', error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
},
});
Ошибка 4: использование больших вложенных объектов
Разрастание структуры хранилища и попытки управлять глубокими вложенными объектами могут привести к проблемам обновления данных. Например, если вам нужно обновить одно поле в глубоко вложенном объекте, это может быть сложно, если вы не используете Immutable.js или аналогичные библиотеки.
Как избежать:
Разделяйте данные на более простые структуры. Если необходимо сохранять связанные данные, используйте нормализацию. Вместо хранения сложного объекта "пользователь" храните его ID и получайте остальные данные из другого среза.
const initialState = {
users: {
byId: {
'1': { id: '1', name: 'Alice' },
'2': { id: '2', name: 'Bob' },
},
allIds: ['1', '2'],
},
};
Ошибка 5: неоптимизированные рендеры компонентов
Если вы используете useSelector, но не оптимизируете селекторы, это может привести к ненужным перерендерингам. Каждый раз, когда происходит изменение состояния, ваш компонент может обновляться даже при отсутствии необходимости.
Как избежать:
Используйте библиотеку Reselect для создания мемоизированных селекторов.
import { createSelector } from 'reselect';
const selectUsers = (state: RootState) => state.users.byId;
const selectActiveUserId = (state: RootState) => state.activeUserId;
export const selectActiveUser = createSelector(
[selectUsers, selectActiveUserId],
(users, activeUserId) => users[activeUserId]
);
Ошибка 6: отсутствие проверки на null или undefined
В компонентах, которые используют данные из Redux, нередки ошибки, связанные с отсутствием проверки на наличие данных. Вы пытаетесь рендерить data.name, а data ещё не загружено.
Как избежать:
Позаботьтесь о ручной проверке в компонентах или настройте дефолтные значения в хранилище.
const userData = useSelector((state: RootState) => state.user.data);
if (!userData) {
return <LoadingIndicator />;
}
return <Text>{userData.name}</Text>;
Разбор сложных случаев
Кейс 1: сложная фильтрация и сортировка данных
Если у вас есть массив данных (например, список продуктов), которые нужно фильтровать и сортировать в разных компонентах, пытаться выполнять эти операции напрямую в Redux может быть дорого. Массивы с большой длиной лучше обрабатывать на уровне компонентов.
Решение:
Сохраняйте исходные данные в Redux, а отфильтрованные или отсортированные данные вычисляйте локально в компоненте.
const products = useSelector((state: RootState) => state.products.all);
const [filter, setFilter] = React.useState('all');
const filteredProducts = React.useMemo(
() => products.filter((product) => product.category === filter),
[products, filter]
);
Кейс 2: подключение Redux Persist для нескольких срезов
Если нужно сохранить только часть состояния, а остальное оставить временным, используйте whitelist или blacklist в Redux Persist.
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth'], // сохраняем только auth
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ