Trwałe zmienianie tematu w React Native za pomocą Context i Hooków
Stwórz przełącznik tematów w React Native przy użyciu Context, useState i useEffect: nawigacja kart, wybieracz tematów, przechowywanie danych w AsyncStorage oraz element ochronny podczas startowania aplikacji.
Zmiana tematu wydaje się być problemem stylizacji, ale w rzeczywistości jest to problem stanu: bieżący temat musi być czytelny z każdego ekranu, możliwy do zmiany z jednego z nich oraz pozostać niezmieniony po ponownym uruchomieniu aplikacji. Dlatego stanowi doskonałe ćwiczenie do nauki Hooków w kontekście czegoś bardziej realistycznego niż zwykły licznik. W tym przewodniku tworzymy małą aplikację React Native z dwoma kartami, wybieraczem tematów oraz trwałym przechowywaniem danych, używając useContext, useState i useEffect zamiast komponentów klasowych oraz dużych ilości kodu szablonowego.
Dlaczego Hooki nadają się do rozwiązania tego problemu
Hooks pojawiły się w React 16.8, a React Native otrzymał stabilne wsparcie dla nich w wersji 0.59. Przed nimi udostępnianie tematu oznaczało użycie dostawcy opartego na klasach do przechowywania stanu, metod życiowych do ładowania zapisanych ustawień oraz właściwości renderujących lub konsumentów rozrzuconych po całym drzewie komponentów. Dzięki Hookom dostawca staje się zwykłym komponentem funkcjonalnym, stan znajduje się w useState, ładowanie odbywa się w useEffect, a konsumenty odczytują wartość za pomocą useContext. Logika znajduje się wtedy w jednym małym pliku, zamiast być rozproszona po metodach życiowych.
Wersje wymienione tutaj wskazują, kiedy po raz pierwszy stało się to możliwe; aktualne wydania React Native obsługują Hooki bez dodatkowych ustawień, więc każdy nowoczesny projekt może z nimi pracować.
Ustawianie projektu
Wymagany jest projekt React Native w wersji 0.59 lub nowszej. Nowy projekt można utworzyć za pomocą polecenia react-native init RNThemeProvider (nowsze narzędzia używają zamiast tego community CLI lub Expo; sprawdź aktualne dokumentacje dotyczące rozpoczęcia pracy).
Dodano również dwie biblioteki:
- react-navigation umożliwia nawigację za pomocą kart.
- AsyncStorage przechowuje wybrany temat na urządzeniu. Został usunięty z jądra React Native i teraz dostępny jest jako oddzielny pakiet społecznościowy (obecnie publikowany pod nazwą
@react-native-async-storage/async-storage), dlatego musi być zainstalowany osobno.
W starszych wersjach React Native moduły natywne również musiały być ręcznie powiązane po instalacji; dzięki automatycznemu łączeniu w nowszych wydaniach krok ten zazwyczaj nie jest konieczny.
Organizacja plików
Niewielka, przewidywalna struktura oddziela logikę tematów od ekranów i nawigacji:
- 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
Folder core zawiera wszystko związane z tematyzacją: themes.json z definicjami tematów oraz themeProvider.js z informacjami o kontekście, dostawcy i pomocniczych funkcjach. Ekranы znajdują się w folderze screens, niestandardowa pasek kart w components, a App.js zarządza nawigacją.
Budowa szkieletu nawigacji
Zacznij bez żadnej tematyzacji. W pliku App.js utwórz navigator z dwiema kartami u dołu: Main dla treści aplikacji oraz Settings dla ustawień. Każda karta odwołuje się do prostego komponentu funkcyjnego w plikach Main.js i Settings.js, który na razie wyświetla jedynie tekst tymczasowy.
W tym etapie uruchom aplikację. Jeśli pojawią się dwie karty, które można przemieniać między sobą, oznacza to, że szkielet jest gotowy, a każdy kolejny krok dodaje jedynie nowe funkcje. API nawigacji zmieniały się w różnych wersjach react-navigation, dlatego należy postępować zgodnie z instrukcjami dotyczącymi aktualnej wersji, którą instalujesz, zamiast dosłownie kopiować starsze przykłady.
Definiowanie tematów i interfejsu wyboru
Tematy jako dane
Każdy temat to obiekt w pliku themes.json zawierający trzy pola: unikalny key identyfikujący go, kolor tła oraz kolor tekstu. Przechowywanie tematów w formacie JSON zamiast kodu ułatwia ich rozbudowę; generator palet kolorów, takich jak Coolors, jest szybkim sposobem na znalezienie par kolorów, które dobrze do siebie pasują.
themeProvider.js importuje ten plik i na razie eksportuje dwie rzeczy: pełny zestaw tematów, który jest wyświetlany na ekranie ustawień, oraz domyślny temat (druga pozycja w tablicy). Logika dostawcy zostanie dodana później.
Ekrany i pasek kart
Na ekranie Ustawienia znajduje się FlatList, który renderuje po jednej wierszu na temat, przy czym nagłówek jest formatowany zgodnie z aktualnym tematem. Ekran Główny również stosuje aktualny temat w swoim tle i tekście.
Na koniec pasek kart powinien również odzwierciedlać wybrany temat. Spersonalizowany komponent TabBar w pliku components/TabBar.js renderuje karty i używa koloru tematu dla aktywnej karty. Jest on rejestrowany w nawigatorze kart w pliku App.js poprzez opcje nawigatora dotyczące spersonalizowanego komponentu paska kart.
W tym momencie aplikacja wygląda na utworzoną według określonego tematu, ale tylko dzięki domyślnemu ustawieniu zapisanemu w kodzie. Nic jeszcze nie reaguje na dotknięcia. Właśnie tu przydają się Hooks.
Dzielenie się tematem za pomocą useContext
React Context umożliwia przepływ wartości przez całą strukturę aplikacji bez konieczności przekazywania propów na każdym poziomie. Jeśli Context jest dla ciebie nowy, dokumentacja React Context wyjaśnia ten mechanizm.
W pliku themeProvider.js utwórz kontekst tematu oraz komponent dostarczający ten kontekst, ThemeContextProvider. Otocz komponent App.js tym dostawcą, aby wszystkie ekrany i pasek kart znajdowały się pod nim.
Aby ułatwić korzystanie z kontekstu, dodaj do tego samego pliku komponent wyższego rzędu withTheme. Ten odczytuje kontekst za pomocą useContext i przekazuje temat do otoczonego komponentu jako właściwość. Zaktualizuj Main, Settings oraz TabBar, aby były eksportowane przez withTheme; w ten sposób otrzymają aktualny temat, nie wiedząc, skąd pochodzi. przewodnik po komponentach wyższego rzędu opisuje ten wzorzec bardziej szczegółowo.
A HOC sprawdza się dobrze, gdy komponenty już oczekują propów. W kodzie opartym na Hooksach mały hook useTheme, który zwraca useContext(ThemeContext), jest często prostszy, unika dodatkowej warstwy otaczającej i sprawia, że zależność jest widoczna wewnątrz komponentu. Naszy przewodnik po custom hooksach i ponownym wykorzystaniu logiki wyjaśnia, dlaczego hooksy dzielą się logiką, a nie stanem, co jest dokładnie powodem, dla którego tutaj nadal potrzebny jest context.
Zmiana tematu za pomocą useState
Teraz sprawdźmy, jak działa wybieracz. Wewnątrz ThemeContextProvider przechowuj aktualny temat w useState, zainicjowany domyślnym tematem. Umieść zarówno temat, jak i funkcję setTheme w wartości contextu.
Na ekranie Ustawienia należy wywołać funkcję setTheme, gdy zostanie kliknięty wiersz w komponencie FlatList. Ponieważ stan dostawcy zmienia się, każdy komponent odczytujący ten kontekst ponownie się renderuje z nowymi kolorami – ekrany oraz pasek kart zmieniają się natychmiast.
Jeden szczegół wart przyjęcia: jeśli wartość kontekstu to nowy obiekt przy każdym renderowaniu dostawcy, wszystkie komponenty konsumujące ten kontekst również się ponownie renderują. Uniknięcie tego problemu polega na zapisywaniu wartości w pamięci za pomocą useMemo, używając tematu jako klucza. W tak małej aplikacji to prawie nie ma znaczenia, ale staje się istotne, gdy wiele komponentów korzysta z tego kontekstu.
Zachowywanie wyboru za pomocą AsyncStorage
Kliknięcie w temat teraz działa, ale wybór znika po ponownym załadowaniu. Rozszerz funkcję setTheme, aby oprócz aktualizacji stanu zapisywała również wybrany temat do AsyncStorage. Zapisywanie key tematu zamiast całego obiektu jest bardziej solidnym rozwiązaniem: jeśli później dostosujesz kolory tematu w pliku themes.json, użytkownicy otrzymają zaktualizowaną wersję, a nie przestarzałą kopię.
Przywracanie tematu przy uruchamianiu za pomocą useEffect
Ostatnim krokiem jest odczytywanie zapisanego tematu po uruchomieniu aplikacji. Funkcja useEffect jest wykonywana po renderowaniu komponentu, więc to właśnie tutaj umieszcza się efekty uboczne, takie jak odczytanie danych z pamięci przechowawczej. W kontekście klasowego podejścia obejmuje to to, co wcześniej zajmowały się funkcje componentDidMount i componentDidUpdate; często przytaczane porównanie z componentWillReceiveProps jest mylące, ponieważ efekty są wykonywane po renderowaniu, a nie przed przybyciem nowych właściwości.
Wewnątrz ThemeContextProvider należy dodać efekt, który odczytuje zapisany klucz z AsyncStorage, znajduje odpowiadający temat i wywołuje funkcję do aktualizacji stanu. Dwa szczegóły zapewniają prawidłowe działanie tego rozwiązania:
- Przekaż pusty tablicę zależności. Efekt powinien zostać wywołany tylko raz, gdy provider zostanie załadowany, a nie po każdym renderowaniu. Referencja API Hooks wyjaśnia, w jaki sposób tablica zależności kontroluje moment wywołania efektu.
- Zadbaj o stan ładowania. AsyncStorage jest asynchroniczne, więc pierwsze renderowanie następuje zanim zostanie ustalone zapisane temat. Sprawdzaj, czy ładowanie zostało zakończone, i nie wyświetlaj nic (lub jedynie widoku tymczasowego) dopóki to nie nastąpi. W przeciwnym razie użytkownicy zobaczą chwilowo domyślny temat, zanim pojawi się ich własny wybór.
Należy również obsłużyć sytuację, gdy nic jeszcze nie zostało zapisane lub zapisany klucz już nie odpowiada żadnemu tematowi, poprzez powrót do domyślnego rozwiązania. Dzięki temu wybrany temat zostanie zachowany po ponownym uruchomieniu aplikacji.
Podsumowanie
Zakończony dostawca to pojedynczy komponent funkcjonalny, który zarządza stanem tematu, przechowuje zmiany, przywraca je podczas uruchamiania i udostępnia wszystko za pośrednictwem kontekstu. W porównaniu z wersją opartą na klasach logika jest krótsza i czytelna od góry do dołu.
Kilka zasad ma zastosowanie również w innych funkcjach:
- Zachowuj wspólne ustawienia interfejsu użytkownika w dostawcy kontekstu znajdującym się blisko korzenia i udostępnij funkcję do ustawiania wartości obok samej wartości.
- Przechowuj identyfikatory, a nie całe obiekty, aby zmiany danych nie pozostawiały przestarzałych kopii na urządzeniach.
- Traktuj asynchroniczne odczyty podczas uruchamiania jako stan ładowania, zamiast renderować domyślne wartości i je później zmieniać.
useReducer do obsługi bardziej złożonego stanu, useRef do wartości zmiennej, które nie powinny wywoływać ponownego renderowania, oraz useLayoutEffect do operacji, które muszą zostać wykonane przed rysowaniem interfejsu. Przekształcanie istniejącego komponentu klasowego to dobry sposób na ćwiczenie.Literatura pokrewna
- Dlaczego Codegen wymusza podejście oparte na specyfikacji w React Native TurboModules — Dowiedz się, jak Codegen przekształca specyfikację TurboModule w TypeScript na umowę używaną podczas budowania, co się psuje, gdy jej pominiesz, oraz gdzie ta zasada przestaje obowiązywać.
- Planowanie aktualizacji do Expo SDK 58: iOS 27, React Native 0.88 oraz nowe narzędzia — Praktyczny przegląd wersji beta Expo SDK 58: jakie zmiany dotyczą iOS 27 i React Native 0.88, które funkcje są eksperymentalne oraz jak bezpiecznie przetestować aktualizację.
- Traktowanie folderów natywnych jako wyniku budowy za pomocą Expo Prebuild i CNG — Jak Continuous Native Generation umożliwia aplikacjom Expo korzystanie z dostosowanych modułów natywnych, pluginów konfiguracyjnych oraz sekretów EAS bez konieczności zapisywania lub ręcznego edytowania folderów ios i android.
- Samodzielnie hostowane aktualizacje OTA dla czystego React Native z hot-updater i Supabase — Konfiguracja aktualizacji JavaScript przez sieć w aplikacji czystego React Native przy użyciu hot-updater i Supabase: init, cichy hook do aktualizacji, kanały, skrypty wdrażania oraz funkcja cofania.