Как управлять 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 для отмены запроса

Общие рекомендации

  1. Изолировать логику вне компонентов
    Храните запросы, таймеры, подписки в сервисах или хуках.

  2. Чётко разделять типы эффектов
    Эффекты UI (анимации, скролл), сетевые, навигационные и пр.

  3. Избегать эффекта "всё в useEffect"
    Вместо огромного хука — создать кастомные хуки или использовать react-query, saga, thunk.

  4. Контролировать жизненный цикл эффекта
    Обязательно очищать таймеры, подписки, запросы в return useEffect.

  5. Логгировать side-effects в dev-сборке
    Используйте loggerMiddleware, консоль или Sentry для аудита побочных эффектов.

  6. Работать с ErrorBoundary и try/catch внутри эффектов
    Ошибки в эффектах не перехватываются автоматически — нужно оборачивать вручную.

  7. Сегментировать эффект по доменам
    Например: authService, notificationService, analyticsService.

Грамотное управление side-effects снижает вероятность багов, упрощает сопровождение и масштабирование приложения. Чем выше изоляция побочных эффектов от UI и логики компонентов, тем легче их повторно использовать, тестировать и отслеживать в продакшене.