Постійна зміна теми в React Native за допомогою Context та Hooks
Створіть перемикач тем для React Native за допомогою Context, useState та useEffect: навігація між вкладками, вибір теми, зберігання даних у AsyncStorage та захист під час завантаження.
Зміна теми здається проблемою стилізації, але насправді це проблема стану: поточна тема має бути читабельною з будь-якого екрана, можливою до зміни з одного з них та залишатися такою після перезапуску додатка. Це робить її гарною практикою для вивчення Hooks у чомусь більш реалістичному, ніж простий лічильник. У цьому посібнику створюється невеликий додаток React Native з двома вкладками, вибірником тем та постійним зберіганням даних, з використанням useContext, useState та useEffect замість класових компонентів та великої кількості шаблонного коду.
Чому Hooks підходять для цієї проблеми
Хуки з’явилися у React 16.8, а React Native отримав стабільну підтримку з версією 0.59. До їх появи для спільного використання теми потрібен був провайдер на основі класів, який зберігав стан, містив методи життєвого циклу для завантаження збережених налаштувань, а також пропси для відображення чи компоненти-споживачі, розкидані по всій структурі проекту. Завдяки хукам провайдер перетворюється на звичайний функціональний компонент, стан зберігається за допомогою useState, завантаження відбувається у useEffect, а споживачі читають значення за допомогою useContext. Логіка тепер знаходиться в одному невеликому файлі, а не розкидана по методах життєвого циклу.
Версії, згадані тут, вказують на момент, коли цей підхід вперше став можливим; сучасні версії React Native підтримують хуки без додаткових налаштувань, тому будь-який сучасний проект працює з ними.
Налаштування проекту
Вам потрібний проєкт React Native версії 0.59 або новішої. Новий проєкт можна створити за допомогою команди react-native init RNThemeProvider (у новіших версіях замість цього використовується CLI спільноти або Expo; перевірте актуальну документацію для початку роботи).
До проєкту додаються ще дві бібліотеки:
- react-navigation забезпечує навігацію за допомогою вкладок.
- AsyncStorage зберігає обраний тематичний режим на пристрої. Його було видалено з основи React Native, і тепер він постачається як окремий пакет спільноти (сьогодні опублікований як
@react-native-async-storage/async-storage), тому його потрібно встановлювати окремо.
У старіших версіях React Native після встановлення також доводилося вручну підключати нативні модулі; у сучасних версіях завдяки автоматичному підключенню цей крок зазвичай не потрібний.
Організація файлів
Невелика, передбачувана структура дозволяє тримати логіку тем окремо від екранів та навігації:
- src
— components
— TabBar.js # custom bottom tabbar component
— core
— themeProvider.js # custom hook for theming
— themes.json # JSON array containing our themes
— screens
— Main.js # first tab
— Settings.js # second tab
— App.js # navigation part
— index.js # entry point for react-native
— package.json # dependencies
У папці core знаходиться все, що стосується тем: themes.json з визначеннями тем та themeProvider.js з контекстом, постачальником та допоміжними функціями. Екрани розташовані у папці screens, власна панель вкладок — у components, а App.js об’єднує всю навігацію.
Створення шелу навігації
Почніть без будь-яких тем. У файлі App.js створіть навігатор вкладок унизу з двома вкладками: Main для контенту додатку та Settings для налаштувань. Кожна вкладка посилається на простий функціональний компонент у Main.js та Settings.js, який наразі відображає лише текст-замінник.
На цьому етапі запустіть додаток. Якщо з’являються дві вкладки, між якими можна перемикатися, то основа готова, а кожен наступний крок лише додає функціонал. API навігації змінювалися у різних версіях react-navigation, тому слід дотримуватися інструкцій для тієї версії, яку ви встановлюєте, а не копіювати старі приклади дослівно.
Визначення тем та інтерфейсу вибору
Теми як дані
Кожна тема — це об’єкт у файлі themes.json, який містить три поля: унікальний key, що ідентифікує її, колір фону та колір тексту. Зберігання тем у форматі JSON замість коду полегшує їх розширення; генератор палітр, такий як Coolors, — це швидкий спосіб знайти пари кольорів, які добре поєднуються.
themeProvider.js імпортує цей файл та наразі експортує два елементи: повний масив тем, який відображається на екрані налаштувань, та стандартну тему (другий елемент масиву). Логіка надання тем буде додана пізніше.
Екрани та панель вкладок
На екрані Налаштування використовується FlatList, який відображає по одному рядку на кожну тему, причому заголовок форматується з урахуванням поточної теми. На екрані Головний також застосовується поточна тема до фону та тексту.
Нарешті, панель вкладок також має відображати тему. Власний компонент TabBar у файлі components/TabBar.js відображає вкладки та використовує колір теми для активної вкладки. Він реєструється у навігаторі вкладок у файлі App.js через параметри навігатора для використання власного компонента панелі вкладок.
На цьому етапі додаток виглядає тематизованим, але лише завдяки зафіксованому за замовчуванням варіанту. Ще ніщо не реагує на дотики. Саме тут і входять у гру Hooks.
Розподіл теми за допомогою useContext
React Context дозволяє передавати значення через всю структуру без необхідності передавати props на кожному рівні. Якщо ви вперше стикаєтесь з Context, документація React Context пояснює цю концепцію.
У файлі themeProvider.js створіть контекст теми та компонент-постачальник ThemeContextProvider. Обгорніть компонент App.js цим постачальником, щоб усі екрани та панель вкладок знаходилися під його керуванням.
Щоб полегшити використання контексту, додайте до того ж файлу компонент вищого порядку withTheme. Він читає контекст за допомогою useContext та передає тему до обгорнутого компонента як параметр. Оновіть Main, Settings та TabBar так, щоб вони експортувалися через withTheme, і вони отримуватимуть поточну тему, не знаючи, звідки вона походить. Посібник з компонентами вищого порядку детальніше розглядає цю практику.
HOC ефективно працює, коли компоненти вже очікують параметрів. У кодбазі, де переважають хуки, невеликий хук useTheme, який повертає useContext(ThemeContext), часто є простішим, уникає додаткового шару обгортки та робить залежність видимою всередині компонента. Наш посібник щодо користувацьких хуків та повторного використання логіки пояснює, чому хуки діляться логікою, а не станом, і саме тому тут все ще потрібен контекст.
Зміна теми за допомогою useState
Тепер зробімо вибірник функціональним. Усередині ThemeContextProvider зберігайте поточну тему за допомогою useState, ініціалізовану за замовчуванням. Додайте до значення контексту як тему, так і функцію setTheme.
На екрані налаштувань викликайте setTheme, коли натискається рядок у FlatList. Оскільки стан постачальника змінюється, кожен компонент, який читає контекст, переробляється з новими кольорами: екрани та панель вкладок миттєво змінюються.
Одна деталь, яку варто застосувати: якщо значення контексту — це новий об’єкт під час кожної переробки постачальника, усі споживачі також переробляються щоразу, коли це робить постачальник. Зберігання значення за допомогою useMemo, прив’язане до теми, уникає цього. У такому невеликому додатку це майже не має значення, але це стає важливим, коли багато компонентів споживають контекст.
Зберігання вибору за допомогою AsyncStorage
Тепер натискання на тему працює, але вибір зникає після перезавантаження. Розширте функцію setTheme, щоб вона, окрім оновлення стану, записувала обраний варіант у AsyncStorage. Зберігання key теми замість всього об’єкта є більш надійним варіантом: якщо пізніше ви зміните кольори теми у файлі themes.json, користувачі отримають оновлену версію замість застарілої копії.
Відновлення теми під час запуску за допомогою useEffect
Останнім кроком є зчитування збереженого теми під час запуску додатка. useEffect виконується після отримання результату рендерингу компонента, тому саме тут можна використовувати побічні ефекти, такі як зчитування даних із пам’яті. З точки зору класового підходу це відповідає тому, що раніше обробляли componentDidMount та componentDidUpdate; іноді наводять порівняння з componentWillReceiveProps, але воно є оманливим, адже ефекти виконуються після рендерингу, а не до отримання нових параметрів.
Усередині ThemeContextProvider додайте ефект, який зчитує збережений ключ із AsyncStorage, знаходить відповідну тему та викликає функцію для зміни стану. Два аспекти забезпечують правильну роботу цього підходу:
- Передайте порожній масив залежностей. Ефект має виконуватися один раз під час ініціалізації компонента, а не після кожного оновлення. Довідник API Hooks пояснює, як масив залежностей контролює момент виконання ефекту.
- Керуйте станом завантаження. AsyncStorage є асинхронним, тому перше оновлення відбувається ще до того, як стає відомий збережений тематичний дизайн. Слідкуйте за тим, чи завершилося завантаження, і не відображайте нічого (або лише тимчасовий інтерфейс) доки це не станеться. Інакше користувачі побачать тимчасово значок стандартної теми перед тим, як з’явиться їхній власний вибір.
Також потрібно врахувати ситуацію, коли ще нічого не збережено або збережений ключ більше не відповідає певній темі, у такому разі слід повернутися до стандартної теми. Це дозволить зберегти вибір користувача після перезапуску додатку.
Підсумок
Готовий провайдер — це єдиний функційний компонент, який керує станом теми, зберігає зміни, відновлює їх під час запуску та робить усе доступним через контекст. Порівняно з версією на основі класів логіка є коротшою та читається зверху вниз.
Кілька принципів застосовуються також до інших функцій:
- Зберігайте спільні налаштування інтерфейсу в провайдері контексту близько до кореня та надавайте функцію для зміни значень разом із самим значенням.
- Зберігайте ідентифікатори, а не цілі об’єкти, щоб зміни даних не залишали старих копій на пристроях.
- Розглядайте асинхронні операції завантаження як стан завантаження, а не як відображення значень за замовчуванням з подальшою заміною.
useReducer для більш складних станів, useRef для змінних значень, які не повинні спричиняти оновлення інтерфейсу, та useLayoutEffect для операцій, які мають виконуватися перед малюванням елементів. Перетворення існуючого компонента класу — хороший спосіб для практики.Пов’язана література
- Чому Codegen робить підхід „спочатку специфікація“ обов’язковим для React Native TurboModules — Дізнайтеся, як Codegen перетворює специфікацію TurboModule на TypeScript у контракт часу збірки, що погіршується при її пропуску та де це правило більше не застосовується.
- Планування оновлення Expo SDK 58: iOS 27, React Native 0.88 та нові інструменти — практичний огляд бета-версії Expo SDK 58: які зміни у iOS 27 та React Native 0.88, які функції є експериментальними та як безпечно протестувати оновлення.
- Використання нативних папок як результату збірки за допомогою Expo Prebuild та CNG — як функція Continuous Native Generation дозволяє додатку Expo використовувати власні нативні модулі, плагіни налаштувань та секрети EAS без необхідності зберігати чи редагувати вручну папки ios та android.
- Самостійно розміщені оновлення OTA для чистого React Native з hot-updater та Supabase — налаштування оновлень JavaScript через мережу у додатку чистого React Native з hot-updater та Supabase: ініціалізація, функція тихого оновлення, канали, скрипти розгортання та скасування змін.