Как управлять side-effects в большом приложении?
Управление побочными эффектами (side-effects) — один из ключевых аспектов разработки масштабируемых React Native приложений. Под побочными эффектами понимаются любые операции, выходящие за пределы «чистого» рендеринга: сетевые запросы, чтение/запись в локальное хранилище, таймеры, подписки на события, логгирование, навигация, вызов нативных API и пр. В большом приложении эти эффекты должны быть изолированы, централизованы и контролируемы, чтобы избежать гонок, конфликтов и утечек памяти.
Что считается побочным эффектом
-
HTTP-запросы (fetch, axios, GraphQL)
-
Чтение/запись в AsyncStorage, SQLite, SecureStore
-
Слушатели событий (подписка на сетевые изменения, клавиатуру, геопозицию)
-
Таймеры (setTimeout, setInterval)
-
Навигация (navigation.navigate(...))
-
Взаимодействие с нативными модулями
-
Обработка push-уведомлений
-
Вызов аналитики, логгирования
Способы управления побочными эффектами
1. Хук useEffect и его вариации
Основной способ выполнять эффекты в функциональных компонентах.
useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/data');
setData(await res.json());
};
fetchData();
}, \[\]);
-
Автоматически выполняется после монтирования или обновления
-
return позволяет очищать эффект
-
Проблемы: эффекты в компонентах могут захламлять код и мешать переиспользуемости
Рекомендации:
-
Инкапсулируй эффекты в кастомные хуки (useFetchUser, useKeyboard)
-
Избегай вложенной логики: выносить в сервисы
2. Кастомные хуки для эффектов
Создание повторно используемых хуков, инкапсулирующих побочные эффекты.
function useKeyboardVisible() {
const \[visible, setVisible\] = useState(false);
useEffect(() => {
const show = Keyboard.addListener('keyboardDidShow', () => setVisible(true));
const hide = Keyboard.addListener('keyboardDidHide', () => setVisible(false));
return () => {
show.remove();
hide.remove();
};
}, \[\]);
return visible;
}
Плюсы:
-
Повторное использование
-
Улучшает читаемость компонента
-
Явное отделение бизнес-логики от UI
3. Сервисы и утилиты вне компонента
Логика side-effect выносится в отдельные модули:
/src
/services
authService.ts
notificationService.ts
api.ts
export const authService = {
login: async (credentials) => {
const response = await axios.post('/login', credentials);
return response.data;
},
};
Компонент вызывает:
useEffect(() => {
authService.login({ email, password }).then(setUser);
}, \[\]);
Преимущества:
-
Тестируемость
-
Простота мокинга
-
Контроль побочных эффектов в одном месте
4. Управление эффектами в Redux
Если используется Redux, side-effects можно централизовать в middleware.
Redux Thunk:
export const fetchUser = () => async (dispatch) => {
dispatch(setLoading(true));
try {
const user = await api.getUser();
dispatch(setUser(user));
} catch (err) {
dispatch(setError(err));
}
};
Плюсы:
-
Простота, интеграция с Redux Toolkit
-
Легко отлаживать и тестировать
Минусы:
- Логика размазывается между actions и компонентами
Redux Saga:
function\* fetchUserSaga() {
try {
const user = yield call(api.getUser);
yield put(setUser(user));
} catch (e) {
yield put(setError(e));
}
}
function\* watchUser() {
yield takeEvery('FETCH_USER', fetchUserSaga);
}
Плюсы:
-
Хорош для сложных цепочек (таймеры, отмена, параллелизм)
-
Эффекты пишутся декларативно
Минусы:
-
Кривая обучения
-
Больше шаблонного кода
Redux Toolkit + RTK Query:
const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
endpoints: (builder) => ({
getUser: builder.query<User, void>({
query: () => 'user',
}),
}),
});
Использование в компоненте:
const { data: user, isLoading } = useGetUserQuery();
Плюсы:
-
Кэширование, рефетчинг, автоматическое обновление
-
Управление побочными эффектами внутри RTK
5. React Query (TanStack Query)
Библиотека для управления асинхронными данными и side-effects:
const { data, isLoading } = useQuery('user', fetchUser);
Дополнительно:
-
Кэш
-
Повтор запросов при ошибке
-
Отмена при размонтировании
-
Фоновое обновление
Для мутаций:
const mutation = useMutation(sendMessage, {
onSuccess: () => queryClient.invalidateQueries('messages'),
});
6. Event Emitter / Pub-Sub
В крупных приложениях может использоваться шаблон событий:
import { EventEmitter } from 'events';
export const eventBus = new EventEmitter();
eventBus.emit('user:logout');
eventBus.on('user:logout', () => clearUserState());
Позволяет разделить исполнение side-effects от вызова, но может быть сложно отлаживать при большом числе событий.
7. Валидация эффектов
Чтобы контролировать лишние вызовы, следует:
-
Использовать useRef для флага isFirstRender
-
Использовать useCallback, useMemo для стабильности ссылок
-
Использовать AbortController или takeLatest в Saga для отмены запроса
Общие рекомендации
-
Изолировать логику вне компонентов
Храните запросы, таймеры, подписки в сервисах или хуках. -
Чётко разделять типы эффектов
Эффекты UI (анимации, скролл), сетевые, навигационные и пр. -
Избегать эффекта "всё в useEffect"
Вместо огромного хука — создать кастомные хуки или использовать react-query, saga, thunk. -
Контролировать жизненный цикл эффекта
Обязательно очищать таймеры, подписки, запросы в return useEffect. -
Логгировать side-effects в dev-сборке
Используйте loggerMiddleware, консоль или Sentry для аудита побочных эффектов. -
Работать с ErrorBoundary и try/catch внутри эффектов
Ошибки в эффектах не перехватываются автоматически — нужно оборачивать вручную. -
Сегментировать эффект по доменам
Например: authService, notificationService, analyticsService.
Грамотное управление side-effects снижает вероятность багов, упрощает сопровождение и масштабирование приложения. Чем выше изоляция побочных эффектов от UI и логики компонентов, тем легче их повторно использовать, тестировать и отслеживать в продакшене.