Мобильные приложенияJavaScriptTypeScript

Первое приложение на React Native и Expo: таймер фокуса на телефоне

Создаём таймер фокуса на React Native и Expo: состояние, интервал, пауза, сброс, форматирование времени и проверка на настоящем телефоне.

Кодик

Автор

7 мин чтения

Expo запускает React Native проект на Android и iOS, а таймер помогает увидеть компоненты, state, effect и cleanup в одном экране. Соберём 25-минутный фокус-таймер с паузой и сбросом, откроем его на телефоне и устраним главный баг первого interval: несколько параллельных отсчётов после повторного нажатия.

Обычный setInterval легко продолжает жить после паузы или размонтирования экрана. В React Native жизненный цикл должен управлять таймером явно. В этой статье вы соберёте мобильный экран с обратным отсчётом 25:00. Финальный результат можно проверить без догадок: Start уменьшает секунды ровно раз в секунду, Pause останавливает, Reset возвращает 25:00, а повторный Start не ускоряет счётчик.

1State

seconds хранит оставшееся время, running отвечает только за режим.

2Effect

useEffect создаёт interval при запуске и очищает его при смене состояния.

3Device

Expo показывает тот же код на реальном телефоне и в симуляторе.

Как Expo запускает React Native приложение

Компонент FocusTimer рендерит View, Text и Pressable. React хранит seconds и running. useEffect подписывается на running, создаёт interval и возвращает cleanup. Формат времени вычисляется при каждом рендере. Для React Native и Expo держите этапы раздельно: ввод, проверка, основное действие и вывод. Такой маршрут позволяет остановиться в конкретной точке, увидеть фактические данные и исправить одну причину вместо случайной перестановки строк.

ЭтапДействиеПроверяемый результат
Созданиеcreate-expo-app готовит проект и routerЕсть запускаемый экран
Стартrunning становится trueEffect создаёт один interval
TickFunctional update уменьшает secondsЭкран показывает новое время
ОчисткаPause или unmount вызывает clearIntervalНет фонового дубликата

Interval является внешним ресурсом, поэтому его время жизни должно совпадать с активным режимом компонента. У React Native и Expo каждый этап оставляет наблюдаемый след, поэтому следующий шаг начинается только после проверки предыдущего.

Жизненный цикл Expo-таймера
От входных данных до видимого результата. Cleanup закрывает каждый созданный interval

Сверяйте SDK проекта и Expo Go. Во время переходов Expo может рекомендовать конкретный SDK для физического устройства. Берите команду из актуального Create a project и не обновляйте package.json вручную частями.

Пишем экран таймера фокуса

Установите 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 специально ограничен одним пользовательским сценарием, чтобы назначение каждой строки оставалось видимым.

Случайный interval и управляемый effect
Что отличает устойчивый проект от случайного запуска. Один 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 для более сложной логики.

Не тестируйте мобильный интерфейс только мышью. Проверьте настоящий телефон: размер текста, safe area, касания, поворот и возвращение из фона. Симулятор не показывает все ограничения устройства.

Проверяем паузу, фон и повторный запуск

Для быстрой проверки временно поставьте FOCUS_SECONDS=10. Нажмите Start дважды, Pause, Start и Reset. Убедитесь, что счётчик не ускоряется. Сверните приложение на пять секунд и запишите фактическое поведение. Затем добавьте короткий break-режим с отдельной константой, но оставьте один источник seconds. Добавьте accessibilityLabel к кнопкам и проверьте крупный системный шрифт. Верните 25 минут перед сохранением проекта.

Проверка своими руками
  1. Проверьте десятисекундный цикл.
  2. Нажмите Start несколько раз.
  3. Поставьте Pause и продолжите.
  4. Сбросьте во время работы.
  5. Сверните приложение и вернитесь.
  6. Проверьте крупный шрифт на телефоне.
Готово, если выполняются все пункты
  • Один 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
3useEffectОчистить interval
4LayoutСобрать View и Pressable
5Жизненный цикл mobileОбработать фон

Сначала добейтесь одного устойчивого экрана, потом добавляйте навигацию и уведомления. Финальная проверка для React Native и Expo: закрыть статью, объяснить путь данных и добавить одну функцию без копирования готового блока.

Создают interval по клику без cleanup

Управляйте им в useEffect, зависящем от running.

Хранят formatted label в state

Вычисляйте строку из seconds при рендере.

Считают ticks точным временем

Для фона пересчитывайте остаток по timestamp.

Проверяют только web preview

Откройте проект на настоящем Android или iOS устройстве.

Сверяйтесь с первичной документацией. Команды и ограничения примера проверены по официальной документации Expo Create a project. Если версия инструмента изменилась, официальный источник важнее старого скриншота.

Что получится в итоге

Готовый результат проекта: Первое приложение на React Native и Expo: таймер фокуса на телефоне
Таймер запускается на телефоне, ставится на паузу, сбрасывается и не создаёт дублирующиеся интервалы.

Короткие ответы
Зачем таймеру 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, а мобильный путь с первым мобильным приложением с ИИ.