Expo запускает React Native проект на Android и iOS, а таймер помогает увидеть компоненты, state, effect и cleanup в одном экране. Соберём 25-минутный фокус-таймер с паузой и сбросом, откроем его на телефоне и устраним главный баг первого interval: несколько параллельных отсчётов после повторного нажатия.
Обычный setInterval легко продолжает жить после паузы или размонтирования экрана. В React Native жизненный цикл должен управлять таймером явно. В этой статье вы соберёте мобильный экран с обратным отсчётом 25:00. Финальный результат можно проверить без догадок: Start уменьшает секунды ровно раз в секунду, Pause останавливает, Reset возвращает 25:00, а повторный Start не ускоряет счётчик.
seconds хранит оставшееся время, running отвечает только за режим.
useEffect создаёт interval при запуске и очищает его при смене состояния.
Expo показывает тот же код на реальном телефоне и в симуляторе.
Как Expo запускает React Native приложение
Компонент FocusTimer рендерит View, Text и Pressable. React хранит seconds и running. useEffect подписывается на running, создаёт interval и возвращает cleanup. Формат времени вычисляется при каждом рендере. Для React Native и Expo держите этапы раздельно: ввод, проверка, основное действие и вывод. Такой маршрут позволяет остановиться в конкретной точке, увидеть фактические данные и исправить одну причину вместо случайной перестановки строк.
| Этап | Действие | Проверяемый результат |
|---|---|---|
| Создание | create-expo-app готовит проект и router | Есть запускаемый экран |
| Старт | running становится true | Effect создаёт один interval |
| Tick | Functional update уменьшает seconds | Экран показывает новое время |
| Очистка | Pause или unmount вызывает clearInterval | Нет фонового дубликата |
Interval является внешним ресурсом, поэтому его время жизни должно совпадать с активным режимом компонента. У React Native и Expo каждый этап оставляет наблюдаемый след, поэтому следующий шаг начинается только после проверки предыдущего.

От входных данных до видимого результата. Cleanup закрывает каждый созданный interval
Пишем экран таймера фокуса
Установите Node.js LTS и Expo Go на телефон. Выполните команду из актуальной документации npx create-expo-app@latest focus-timer, перейдите в каталог и запустите npx expo start. Телефон и компьютер должны видеть dev-сервер; при проблемах используйте предложенный tunnel-режим. В default-проекте замените стартовый экран на код статьи.
import { useEffect, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
const FOCUS_SECONDS = 25 * 60;
export default function FocusTimer() {
const [seconds, setSeconds] = useState(FOCUS_SECONDS);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const interval = setInterval(() => {
setSeconds((current) => {
if (current <= 1) {
setRunning(false);
return 0;
}
return current - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [running]);
const minutes = Math.floor(seconds / 60);
const rest = seconds % 60;
const label = String(minutes).padStart(2, "0") + ":" + String(rest).padStart(2, "0");
function reset() {
setRunning(false);
setSeconds(FOCUS_SECONDS);
}
return (
<View style={styles.screen}>
<Text style={styles.caption}>FOCUS SESSION</Text>
<Text style={styles.timer}>{label}</Text>
<View style={styles.row}>
<Pressable style={styles.primary} onPress={() => setRunning(!running)}>
<Text style={styles.primaryText}>{running ? "Pause" : "Start"}</Text>
</Pressable>
<Pressable style={styles.secondary} onPress={reset}>
<Text>Reset</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#F5F7FF" },
caption: { color: "#6371FF", fontWeight: "800", letterSpacing: 2 },
timer: { fontSize: 76, fontWeight: "900", marginVertical: 28, fontVariant: ["tabular-nums"] },
row: { flexDirection: "row", gap: 12 },
primary: { backgroundColor: "#6371FF", paddingHorizontal: 28, paddingVertical: 15, borderRadius: 16 },
primaryText: { color: "white", fontWeight: "800" },
secondary: { backgroundColor: "white", paddingHorizontal: 28, paddingVertical: 15, borderRadius: 16 },
});- Старт создаёт ровно один interval.
- Functional update использует актуальные seconds.
- Пауза вызывает cleanup и фиксирует значение.
- На нуле running переключается в false.
Форматирование label остаётся вычислением, а не отдельным state, поэтому минуты и секунды не расходятся. Пример React Native и Expo специально ограничен одним пользовательским сценарием, чтобы назначение каждой строки оставалось видимым.

Что отличает устойчивый проект от случайного запуска. Один state определяет экран
Почему interval живёт внутри useEffect
React Native использует нативные компоненты View и Text вместо HTML-тегов. Metro собирает JavaScript/TypeScript и отправляет bundle в клиент разработки. useEffect выполняется после рендера и очищает предыдущий interval перед новым. Functional update получает актуальное значение даже внутри callback. Когда приложение уходит в фон, интервалы могут замедляться или приостанавливаться, поэтому точный production-таймер лучше считать от timestamp окончания, а не доверять числу ticks.
| Часть | За что отвечает | Что проверить |
|---|---|---|
| View/Text | Создают нативный UI | Нет div и h1 |
| useState | Хранит seconds и running | Нет дублирующего label |
| useEffect | Управляет interval | Возвращает cleanup |
| Pressable | Принимает касание | Кнопка имеет ясный текст |
| StyleSheet | Группирует стили | Layout проверен на экране |
setInterval не гарантирует точность до миллисекунды. Для фонового режима сохраните endAt=Date.now()+seconds*1000 и пересчитайте остаток при возвращении через AppState. Не держите interval id в state: он не участвует в интерфейсе, достаточно локальной переменной effect или ref для более сложной логики.
Проверяем паузу, фон и повторный запуск
Для быстрой проверки временно поставьте FOCUS_SECONDS=10. Нажмите Start дважды, Pause, Start и Reset. Убедитесь, что счётчик не ускоряется. Сверните приложение на пять секунд и запишите фактическое поведение. Затем добавьте короткий break-режим с отдельной константой, но оставьте один источник seconds. Добавьте accessibilityLabel к кнопкам и проверьте крупный системный шрифт. Верните 25 минут перед сохранением проекта.
- Проверьте десятисекундный цикл.
- Нажмите Start несколько раз.
- Поставьте Pause и продолжите.
- Сбросьте во время работы.
- Сверните приложение и вернитесь.
- Проверьте крупный шрифт на телефоне.
- Один interval.
- Пауза стабильна.
- Ноль останавливает таймер.
- Телефон отображает экран.
Мобильный таймер понятен, когда вы можете объяснить, кто создаёт interval, кто его очищает и как восстанавливается время. После этой проверки измените одно правило React Native и Expo и заранее запишите ожидаемый результат. Так копирование превращается в понимание.

Четыре проверки перед следующим шагом. Фон проверяется на устройстве
Как развивать мобильный проект дальше
Дальше добавьте AppState и timestamp, затем локальные уведомления. Историю сессий храните отдельно от текущего countdown. Для нескольких экранов используйте router, но не переносите state без причины в глобальное хранилище. Перед store-build проверьте иконки, splash, permissions и privacy. Expo Go подходит для обучения, а нативные модули и production требуют development build или EAS Build по актуальной документации.
Как изучить тему в Кодике
Повторите функции, состояние, массивы и интервалы JavaScript, затем сравните HTML-элементы с React Native components. В Кодике сначала восстановите чистую логику без фреймворка, затем верните библиотеку и интерфейс React Native и Expo.
| Шаг | Что разобрать | Мини-проверка |
|---|---|---|
| 1 | Функции и state | Переключить running |
| 2 | Время и строки | Собрать 05:09 |
| 3 | useEffect | Очистить interval |
| 4 | Layout | Собрать View и Pressable |
| 5 | Жизненный цикл mobile | Обработать фон |
Сначала добейтесь одного устойчивого экрана, потом добавляйте навигацию и уведомления. Финальная проверка для React Native и Expo: закрыть статью, объяснить путь данных и добавить одну функцию без копирования готового блока.
Управляйте им в useEffect, зависящем от running.
Вычисляйте строку из seconds при рендере.
Для фона пересчитывайте остаток по timestamp.
Откройте проект на настоящем Android или iOS устройстве.
Что получится в итоге

Таймер запускается на телефоне, ставится на паузу, сбрасывается и не создаёт дублирующиеся интервалы.
Зачем таймеру cleanup в useEffect?
Cleanup удаляет прежний interval при паузе, повторном effect и размонтировании экрана.
Почему label не хранится в useState?
Строка времени полностью вычисляется из seconds и не является отдельным источником данных.
Точен ли setInterval в фоне телефона?
Нет, фон может замедлить или остановить ticks, поэтому точный таймер восстанавливают по timestamp.
Как понять, что учебный проект React Native и Expo действительно работает?
Проверьте основной сценарий React Native и Expo, неверный ввод, повторный запуск и один граничный случай. Затем объясните путь данных своими словами.
Можно ли начать с готового кода React Native и Expo из статьи?
Да. Сначала добейтесь результата React Native и Expo, затем измените одно правило и восстановите ключевой файл без копирования.
JavaScript и функции закрепите в курсе JavaScript, а компоненты сравните с React-проектом на Vite.
Логику отсчёта сопоставьте с таймером на JavaScript, а мобильный путь с первым мобильным приложением с ИИ.