
React Native + Redux: управление состоянием
Redux остаётся надёжным выбором для масштабируемого управления состоянием в мобильных приложениях на React Native. С современным Redux Toolkit (RTK) он стал проще, безопаснее и короче. Ниже — практическое руководство: когда Redux действительно нужен, как его настроить в RN‑проекте, как работать с асинхронными эффектами, кэшировать серверные данные и не потерять производительность.
1. Когда выбирать Redux в React Native
- Сложная бизнес‑логика: много источников данных, сложные преобразования, права доступа, офлайн‑режим.
- Кросс‑экранные сценарии: одни и те же данные используются в разных местах (профиль, корзина, настройки).
- Прозрачность и инспекция: тайм‑тревел, логирование, сериализация — важно при сложном дебаге.
- Команда/масштаб: явные правила, неизменяемость, единый стор упрощают коллективную разработку.
Если состояние преимущественно «серверное» (списки, карточки, пагинация) — рассмотрите RTK Query или TanStack Query. Redux удобен для «клиентского» состояния: авторизация, локальные настройки, сложные формы‑мастера, кэш офлайна, очередь действий.
2. Установка и базовая структура
npm i @reduxjs/toolkit react-redux
Минимальная структура:
src/store/index.ts
— создание стораsrc/features/<domain>/<domain>.slice.ts
— слайс доменаsrc/app/App.tsx
— подключениеProvider
3. Создание стора через RTK
Нужны безопасные значения по умолчанию, строгая типизация и селекторы — это обеспечивает RTK.
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'
import authReducer from '../features/auth/auth.slice'
export const store = configureStore({
reducer: {
auth: authReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Пояснение:
configureStore
сразу включает полезные миддлвары и строгую проверку immutable/serializable в дев‑режиме.- Типизированные хуки убирают ручные дженерики во всех компонентах.
4. Слайс: состояние аутентификации
Не усложняйте. Начните с малого и расширяйте по мере роста требований.
// src/features/auth/auth.slice.ts
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'
type AuthState = {
token: string | null
loading: boolean
error: string | null
}
const initialState: AuthState = {
token: null,
loading: false,
error: null,
}
export const login = createAsyncThunk(
'auth/login',
async (args: { email: string; password: string }) => {
// Здесь будет реальный вызов API
const res = await fetch('https://example.com/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args),
})
if (!res.ok) throw new Error('Неверные учётные данные')
const data = (await res.json()) as { token: string }
return data.token
}
)
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout(state) {
state.token = null
state.error = null
},
},
extraReducers: builder => {
builder
.addCase(login.pending, state => {
state.loading = true
state.error = null
})
.addCase(login.fulfilled, (state, action: PayloadAction<string>) => {
state.loading = false
state.token = action.payload
})
.addCase(login.rejected, (state, action) => {
state.loading = false
state.error = action.error.message ?? 'Ошибка входа'
})
},
})
export const { logout } = authSlice.actions
export default authSlice.reducer
Ключевые мысли:
createAsyncThunk
упрощает эффекты: loading/error/fulfilled обрабатываются в одном месте.- Состояние лаконично и предсказуемо; всё, что касается auth, держите в одном слайсе.
5. Подключение к корню приложения
В React Native провайдер ставится в корневой компонент (например, App.tsx
).
// src/app/App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { store } from '../store'
import { View, Text } from 'react-native'
export default function App() {
return (
<Provider store={store}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Приложение с Redux готово</Text>
</View>
</Provider>
)
}
Использование в экране:
// пример экрана
import React, { useState } from 'react'
import { View, Text, Button, TextInput } from 'react-native'
import { useAppDispatch, useAppSelector } from '../store'
import { login, logout } from '../features/auth/auth.slice'
export function AuthScreen() {
const dispatch = useAppDispatch()
const { token, loading, error } = useAppSelector(s => s.auth)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
return (
<View style={{ padding: 16 }}>
{token ? (
<>
<Text>Токен: {token.slice(0, 6)}...</Text>
<Button title="Выйти" onPress={() => dispatch(logout())} />
</>
) : (
<>
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
<TextInput placeholder="Пароль" value={password} onChangeText={setPassword} secureTextEntry />
<Button title={loading ? 'Вход...' : 'Войти'} onPress={() => dispatch(login({ email, password }))} />
{!!error && <Text style={{ color: 'red' }}>{error}</Text>}
</>
)}
</View>
)
}
Замечания:
- Избегайте хранения больших объектов (например, ответа профиля целиком) — сохраняйте ключи/токены, а данные запрашивайте по идентификатору.
- Для списков используйте
FlatList
, чтобы не инициировать лишние ре‑рендеры.
6. Сохранение состояния с Redux Persist
Аутентификация и пользовательские настройки полезно переживают перезапуск приложения. Для RN используйте AsyncStorage
как движок.
npm i redux-persist @react-native-async-storage/async-storage
// src/store/index.ts (фрагмент)
import AsyncStorage from '@react-native-async-storage/async-storage'
import { persistStore, persistReducer } from 'redux-persist'
import authReducer from '../features/auth/auth.slice'
const authPersistConfig = {
key: 'auth',
storage: AsyncStorage,
whitelist: ['token'],
}
const rootReducer = {
auth: persistReducer(authPersistConfig, authReducer),
}
export const store = configureStore({ reducer: rootReducer })
export const persistor = persistStore(store)
И обёртка в корне:
// src/app/App.tsx (фрагмент)
import { PersistGate } from 'redux-persist/integration/react'
import { persistor, store } from '../store'
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{/* ваш навигатор/экраны */}
</PersistGate>
</Provider>
Советы:
- В
whitelist
перечисляйте только действительно нужные ключи, чтобы не сохранять лишнее. - Храните секреты (токены) в
SecureStore
/Keychain, если это требование безопасности; сочетайте с Persist через синхронизацию при старте.
7. RTK Query для серверных данных
Если в приложении много запросов к API и кэша, RTK Query закроет 80% задач без ручного createAsyncThunk
.
npm i @reduxjs/toolkit
// src/features/api/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/api' }),
endpoints: builder => ({
getPosts: builder.query<{ id: number; title: string }[], void>({
query: () => '/posts?_limit=20',
}),
}),
})
export const { useGetPostsQuery } = api
Подключение:
// src/store/index.ts (фрагмент)
import { api } from '../features/api/api'
export const store = configureStore({
reducer: {
auth: persistReducer(authPersistConfig, authReducer),
[api.reducerPath]: api.reducer,
},
middleware: getDefault => getDefault().concat(api.middleware),
})
Использование:
// любой экран
import { useGetPostsQuery } from '../features/api/api'
function PostsScreen() {
const { data, isLoading, refetch } = useGetPostsQuery()
// отрисовка списка с учётом загрузки/ошибок
}
Преимущества:
- Автокэш, инвалидация, рефетч по фокусу/сетям — без вспомогательного кода.
- Для RN работает из коробки на
fetch
, не требует дополнительных зависимостей.
8. Производительность и архитектура
- Мемоизация селекторов: для дорогих вычислений используйте
reselect
илиcreateSelector
из RTK. - Точечные селекторы: выбирайте только необходимые поля в
useAppSelector
, а не целые ветки состояния. - Нормализация: храните коллекции как
byId + allIds
, чтобы обновления затрагивали меньше компонентов. - Разделение стора по доменам:
auth
,profile
,cart
,settings
. Держите границы чёткими, чтобы проще было следить за побочными эффектами. - Отладка: в RN удобно использовать Flipper с плагином Redux. Для Expo — Remote JS Debugging/Flipper через dev‑меню.
9. Частые ошибки
- Смешение «серверного» и «клиентского» состояния в одном слайсе. Держите кэш API отдельно (RTK Query) от локальной логики.
- Сохранение чрезмерного объёма данных в Persist (лишние мегабайты и долгий старт).
- Глобализация всего подряд: локальное UI‑состояние (открыт ли модал) храните в компоненте/навигации, а не в Redux.
- Отсутствие типизации хуков селектора/диспатча — ведёт к слабым подсказкам и скрытым ошибкам.
10. Чек‑лист интеграции
- Установлены
@reduxjs/toolkit
иreact-redux
, создан стор черезconfigureStore
. - Слайсы по доменам, асинхронные операции через
createAsyncThunk
(или RTK Query для API). - Типизированы
RootState
,AppDispatch
, добавленыuseAppSelector
/useAppDispatch
. - Persist настроен для критичных данных (токен/настройки), белый список минимален.
- Селекторы узкие и по возможности мемоизированы.
- Производительность проверена на реальных списках (
FlatList
), лишние ре‑рендеры устранены.
Итоги
С современным Redux Toolkit интеграция Redux в React Native стала заметно проще: меньше шаблонного кода, лучше типизация, встроенные инструменты для асинхронности и кэширования. Используйте Redux для сложной клиентской логики и сквозного состояния, а для серверных данных — RTK Query. Сдерживайте объём Persist, проектируйте слайсы по доменам и следите за производительностью — так вы получите предсказуемую и масштабируемую архитектуру.