Tworzenie aplikacji internetowej za pomocą React, Redux i Sanity.io

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Headless CMS to potężny i łatwy sposób na zarządzanie treścią i dostęp do API. Zbudowany na React, Sanity.io jest płynnym narzędziem do elastycznego zarządzania treścią. Może być używany do budowania od podstaw prostych do złożonych aplikacji. W tym artykule, Ifeanyi wyjaśnia, jak zbudować prostą aplikację z listą przy użyciu Sanity.io i React. Globalne stany będą zarządzane przez Redux, a aplikacja będzie stylizowana za pomocą styled-components.

Szybka ewolucja platform cyfrowych nałożyła poważne ograniczenia na tradycyjne systemy CMS, takie jak Wordpress. Platformy te są połączone, nieelastyczne i koncentrują się na projekcie, a nie na produkcie. Na szczęście opracowano kilka bezgłowych CMS, aby sprostać tym wyzwaniom i wielu innym.

W przeciwieństwie do tradycyjnego CMS, bezgłowy CMS, który można opisać jako oprogramowanie jako usługę (SaaS), może być używany do tworzenia stron internetowych, aplikacji mobilnych, wyświetlaczy cyfrowych i wielu innych. Mogą być używane na nieograniczonych platformach. Jeśli szukasz CMS, który jest niezależny od platformy, zorientowany na programistę i oferuje wsparcie dla wielu platform, nie musisz szukać dalej od bezgłowego CMS.

Bezgłowy CMS to po prostu CMS bez głowy. head odnosi się tutaj do frontendu lub warstwy prezentacji, podczas gdy body odnosi się do backendu lub repozytorium treści. Daje to wiele ciekawych korzyści. Na przykład pozwala programiście wybrać dowolny interfejs użytkownika, a także zaprojektować warstwę prezentacji według własnego uznania.

Istnieje wiele bezgłowych CMS, niektóre z najbardziej popularnych to Strapi, Contentful, Contentstack, Sanity, Butter CMS, Pryzmat, Storyblok, Directus itp. Te bezgłowe CMS są oparte na API i mają swoje mocne strony. Na przykład CMS, takie jak Sanity, Strapi, Contentful i Storyblok, są bezpłatne dla małych projektów.

Te bezgłowe CMS są również oparte na różnych stosach technologicznych. Podczas gdy Sanity.io bazuje na React.js, Storyblok bazuje na Vue.js. Jako programista React jest to główny powód, dla którego szybko zainteresowałem się Sanity. Jednak będąc bezgłowym CMS, każdą z tych platform można podłączyć do dowolnego frontendu, czy to Angular, Vue czy React.

Każdy z tych bezgłowych CMS ma zarówno darmowe, jak i płatne plany, które stanowią znaczny skok cen. Chociaż te płatne plany oferują więcej funkcji, nie chciałbyś płacić tak dużo za mały lub średni projekt. Sanity próbuje rozwiązać ten problem, wprowadzając opcje płatności zgodnie z rzeczywistym użyciem. Dzięki tym opcjom będziesz mógł zapłacić za to, z czego korzystasz i uniknąć skoku cenowego.

Innym powodem, dla którego wybieram Sanity.io, jest ich język GROQ. Dla mnie Sanity wyróżnia się z tłumu, oferując to narzędzie. Graficzno-relacyjne zapytania obiektów (GROQ) skracają czas opracowywania, pomagają uzyskać potrzebną zawartość w potrzebnej formie, a także pomagają deweloperom tworzyć dokument z nowym modelem zawartości bez zmian w kodzie.

Co więcej, programiści nie są ograniczeni do języka GROQ. Możesz także użyć GraphQL lub nawet tradycyjnego axios i fetch w swojej aplikacji React, aby wysłać zapytanie do backendu. Podobnie jak większość innych bezgłowych CMS, Sanity ma obszerną dokumentację, która zawiera przydatne wskazówki dotyczące budowania na platformie.

Uwaga: ten artykuł wymaga podstawowej wiedzy na temat React, Redux i CSS.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Pierwsze kroki z Sanity.io

Aby używać Sanity na swoim komputerze, musisz zainstalować narzędzie Sanity CLI. Chociaż można go zainstalować lokalnie w projekcie, lepiej jest zainstalować go globalnie, aby udostępnić go przyszłym aplikacjom.

Aby to zrobić, wprowadź następujące polecenia w swoim terminalu.

 npm install -g @sanity/cli

Flaga -g w powyższym poleceniu umożliwia instalację globalną.

Następnie musimy zainicjować Sanity w naszej aplikacji. Chociaż można go zainstalować jako osobny projekt, zwykle lepiej jest zainstalować go w aplikacji frontendowej (w tym przypadku React).

Na swoim blogu Kapehe szczegółowo wyjaśniła, jak zintegrować Sanity z React. Pomocne będzie przejrzenie artykułu przed kontynuowaniem tego samouczka.

Wprowadź następujące polecenia, aby zainicjować Sanity w aplikacji React.

 sanity init

Polecenie sanity staje się dla nas dostępne po zainstalowaniu narzędzia Sanity CLI. Listę dostępnych poleceń Sanity można wyświetlić, wpisując w terminalu sanity help dotyczącą zdrowia sanity lub zdrowia psychicznego.

Podczas konfigurowania lub inicjowania projektu musisz postępować zgodnie z instrukcjami, aby go dostosować. Będziesz także musiał utworzyć zestaw danych, a nawet możesz wybrać niestandardowy zestaw danych wypełniony danymi. W przypadku tej aplikacji do tworzenia ofert użyjemy niestandardowego zestawu danych filmów science fiction firmy Sanity. Uchroni nas to przed samodzielnym wprowadzaniem danych.

Aby wyświetlić i edytować swój zbiór danych, cd do podkatalogu Sanity w terminalu i wpisz sanity start . To zwykle działa na https://localhost:3333/ . Może być wymagane zalogowanie się, aby uzyskać dostęp do interfejsu (upewnij się, że logujesz się na to samo konto, którego użyłeś podczas inicjowania projektu). Zrzut ekranu środowiska znajduje się poniżej.

Przegląd serwera Sanity
Przegląd serwera sanity dla zestawu danych filmu science fiction. (duży podgląd)

Dwukierunkowa komunikacja Sanity-React

Sanity i React muszą komunikować się ze sobą, aby aplikacja była w pełni funkcjonalna.

Ustawienie CORS Origins w Sanity Manager

Najpierw połączymy naszą aplikację React z Sanity. Aby to zrobić, zaloguj się na https://manage.sanity.io/ i znajdź CORS origins w API Settings w zakładce Settings . Tutaj musisz połączyć swój frontend z backendem Sanity. Nasza aplikacja React domyślnie działa na https://localhost:3000/ , więc musimy dodać to do CORS.

Pokazuje to poniższy rysunek.

Ustawienia pochodzenia CORS
Ustawianie pochodzenia CORS w Sanity.io Manager. (duży podgląd)

Łączenie zdrowia psychicznego z reakcją

Sanity przypisuje project ID do każdego tworzonego projektu. Ten identyfikator jest potrzebny podczas łączenia go z aplikacją frontendową. Możesz znaleźć identyfikator projektu w swoim Sanity Manager.

Backend komunikuje się z Reactem za pomocą biblioteki znanej jako sanity client . Musisz zainstalować tę bibliotekę w swoim projekcie Sanity, wprowadzając następujące polecenia.

 npm install @sanity/client

Utwórz plik sanitySetup.js (nazwa pliku nie ma znaczenia) w folderze src projektu i wprowadź następujące kody React, aby skonfigurować połączenie między Sanity i React.

 import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });

Przekazaliśmy nasz projectId , dataset name i wartość logiczną useCdn do instancji klienta sanity zaimportowanego z @sanity/client . To działa magicznie i łączy naszą aplikację z zapleczem.

Teraz, gdy zakończyliśmy dwukierunkowe połączenie, przejdźmy od razu do budowania naszego projektu.

Konfigurowanie i podłączanie Redux do naszej aplikacji

Będziemy potrzebować kilku zależności do pracy z Redux w naszej aplikacji React. Otwórz terminal w środowisku React i wprowadź następujące polecenia bash.

 npm install redux react-redux redux-thunk

Redux to globalna biblioteka zarządzania stanem, która może być używana z większością frontendowych frameworków i bibliotek, takich jak React. Potrzebujemy jednak pośredniczącego narzędzia react-redux aby umożliwić komunikację między naszym sklepem Redux a naszą aplikacją React. Redux thunk pomoże nam zwrócić funkcję zamiast obiektu akcji z Redux.

Chociaż moglibyśmy napisać cały przepływ pracy Redux w jednym pliku, często lepiej jest oddzielić nasze obawy. W tym celu podzielimy nasz przepływ pracy na trzy pliki a mianowicie actions , reducers , a następnie store . Jednak potrzebujemy również osobnego pliku do przechowywania action types , znanych również jako constants .

Konfiguracja sklepu

Sklep jest najważniejszym plikiem w Redux. Organizuje i pakuje stany oraz wysyła je do naszej aplikacji React.

Oto początkowa konfiguracja naszego sklepu Redux potrzebna do połączenia naszego przepływu pracy Redux.

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );

Funkcja createStore w tym pliku przyjmuje trzy parametry: reducer (wymagany), stan początkowy i wzmacniacz (zwykle oprogramowanie pośrednie, w tym przypadku thunk dostarczane przez applyMiddleware ). Nasze reduktory będą przechowywane w folderze reducers , a połączymy je i wyeksportujemy w index.js w folderze reducers . To jest plik, który zaimportowaliśmy w powyższym kodzie. Wrócimy do tego pliku później.

Wprowadzenie do języka GROQ firmy Sanity

Sanity przenosi zapytania dotyczące danych JSON o krok dalej, wprowadzając GROQ. GROQ to skrót od Graph-Relational Object Queries. Według Sanity.io, GROQ jest deklaratywnym językiem zapytań zaprojektowanym do odpytywania kolekcji dokumentów JSON w dużej mierze pozbawionych schematów.

Sanity zapewnia nawet GROQ Playground , aby pomóc programistom zapoznać się z językiem. Jednak, aby uzyskać dostęp do placu zabaw, musisz zainstalować wizję zdrowia psychicznego . Uruchom sanity install @sanity/vision na swoim terminalu, aby go zainstalować.

GROQ ma podobną składnię do GraphQL, ale jest bardziej skondensowany i łatwiejszy do odczytania. Co więcej, w przeciwieństwie do GraphQL, GROQ może być używany do wysyłania zapytań o dane JSON.

Na przykład, aby pobrać każdy element w naszym dokumencie filmowym, użyjemy następującej składni GROQ.

 *[_type == "movie"]

Jednakże, jeśli chcemy pobrać tylko _ids i crewMembers z naszego dokumentu filmowego. Musimy określić te pola w następujący sposób.

 `*[_type == 'movie']{ _id, crewMembers }

Tutaj użyliśmy * , aby powiedzieć GROQ, że potrzebujemy każdego dokumentu filmu _type . _type to atrybut w kolekcji filmów. Możemy również zwrócić typ, tak jak zrobiliśmy to z _id i crewMembers w następujący sposób:

 *[_type == 'movie']{ _id, _type, crewMembers }

Będziemy więcej pracować nad GROQ, wdrażając go w naszych akcjach Redux, ale możesz sprawdzić dokumentację Sanity.io dla GROQ, aby dowiedzieć się więcej na ten temat. Ściągawka z zapytaniami GROQ zawiera wiele przykładów, które pomogą Ci opanować język zapytań.

Konfigurowanie stałych

Potrzebujemy stałych do śledzenia typów akcji na każdym etapie przepływu pracy Redux. Stałe pomagają określić typ akcji wykonywanej w każdym momencie. Na przykład możemy śledzić, kiedy API jest ładowane, w pełni załadowane i kiedy wystąpi błąd.

Niekoniecznie musimy definiować stałe w osobnym pliku, ale dla uproszczenia i przejrzystości jest to zwykle najlepsza praktyka w Redux.

Zgodnie z konwencją, stałe w JavaScript są definiowane wielkimi literami. Będziemy postępować zgodnie z najlepszymi praktykami tutaj, aby zdefiniować nasze stałe. Oto przykład stałej oznaczającej żądania przeniesienia filmu do pobrania.

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";

Tutaj utworzyliśmy stałą MOVIE_FETCH_REQUEST , która oznacza typ akcji MOVIE_FETCH_REQUEST . Pomaga nam to łatwo wywołać ten typ akcji bez używania strings i uniknąć błędów. Wyeksportowaliśmy również stałą, aby była dostępna w dowolnym miejscu naszego projektu.

Podobnie możemy utworzyć inne stałe do pobierania typów akcji oznaczających, kiedy żądanie się powiedzie lub nie. Pełny kod dla movieConstants.js znajduje się w poniższym kodzie.

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";

Tutaj zdefiniowaliśmy kilka stałych do pobierania filmu lub listy filmów, sortowania i pobierania najpopularniejszych filmów. Zauważ, że ustawiamy stałe, aby określić, kiedy żądanie jest loading , successful i failed .

Podobnie nasz plik personConstants.js znajduje się poniżej:

 export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";

Podobnie jak movieConstants.js , ustawiamy listę stałych do pobierania osoby lub osób. Ustalamy również stałą do liczenia osób. Stałe są zgodne z konwencją opisaną dla movieConstants.js , a także wyeksportowaliśmy je, aby były dostępne dla innych części naszej aplikacji.

Na koniec zaimplementujemy w aplikacji tryb jasny i ciemny, dzięki czemu mamy kolejny plik stałych globalConstants.js . Przyjrzyjmy się temu.

 export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";

Tutaj ustawiamy stałe, aby określić, kiedy uruchamiany jest tryb jasny lub ciemny. SET_LIGHT_THEME określa, kiedy użytkownik przełącza się na jasny motyw, a SET_DARK_THEME określa, kiedy wybierany jest ciemny motyw. Wyeksportowaliśmy również nasze stałe, jak pokazano.

Konfigurowanie działań

Zgodnie z konwencją nasze działania są przechowywane w osobnym folderze. Akcje są pogrupowane według ich typów. Na przykład, nasze akcje filmowe są przechowywane w movieActions.js , podczas gdy nasze akcje osobowe są przechowywane w pliku personActions.js .

Mamy również globalActions.js , który zajmuje się przełączaniem motywu z trybu jasnego na ciemny.

Pobierzmy wszystkie filmy z moviesActions.js .

 import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };

Pamiętasz, jak stworzyliśmy plik sanitySetup.js , aby połączyć React z naszym backendem Sanity? Tutaj zaimportowaliśmy konfigurację, aby umożliwić nam wysyłanie zapytań do naszego zaplecza psychicznego za pomocą GROQ. Zaimportowaliśmy również kilka stałych wyeksportowanych z pliku movieConstants.js w folderze constants .

Następnie utworzyliśmy funkcję akcji fetchAllMovies do pobierania każdego filmu z naszej kolekcji. Większość tradycyjnych aplikacji React używa axios lub fetch do pobierania danych z zaplecza. Ale chociaż moglibyśmy użyć dowolnego z nich tutaj, używamy GROQ Sanity . Aby wejść w tryb GROQ , musimy wywołać sanityAPI.fetch() , jak pokazano w powyższym kodzie. Tutaj sanityAPI to połączenie React-Sanity, które skonfigurowaliśmy wcześniej. Zwraca to Promise i dlatego musi być wywołane asynchronicznie. Użyliśmy tutaj składni async-await await, ale możemy również użyć składni .then .

Ponieważ używamy thunk w naszej aplikacji, możemy zwrócić funkcję zamiast obiektu akcji. Zdecydowaliśmy się jednak przekazać oświadczenie return w jednym wierszu.

 const fetchAllMovies = () => async (dispatch) => { ... }

Zauważ, że możemy również napisać funkcję w ten sposób:

 const fetchAllMovies = () => { return async (dispatch)=>{ ... } }

Ogólnie rzecz biorąc, aby pobrać wszystkie filmy, najpierw wysłaliśmy typ akcji, który śledzi, kiedy żądanie jest nadal wczytywane. Następnie użyliśmy składni GROQ Sanity do asynchronicznego zapytania o dokument filmowy. _id i adres URL plakatu danych filmu. Następnie zwróciliśmy ładunek zawierający dane pobrane z interfejsu API.

Podobnie możemy pobrać filmy według ich _id , posortować filmy i uzyskać najpopularniejsze filmy.

Możemy również pobrać filmy pasujące do referencji konkretnej osoby. Zrobiliśmy to w funkcji fetchMoviesByRef .

 const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };

Ta funkcja pobiera argument i sprawdza, czy person._ref w castMembers lub crewMembers pasuje do przekazanego argumentu. Zwracamy obok identyfikatora _id , poster url i title . Wysyłamy również akcję typu MOVIES_REF_FETCH_SUCCESS , dołączając ładunek zwróconych danych, a jeśli wystąpi błąd, wysyłamy akcję typu MOVIE_REF_FETCH_FAIL , dołączając ładunek komunikatu o błędzie, dzięki wrapperowi try-catch .

W funkcji fetchMovieById użyliśmy GROQ do pobrania filmu pasującego do określonego id przekazanego do funkcji.

Składnia GROQ dla funkcji jest pokazana poniżej.

 const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );

Podobnie jak w przypadku akcji fetchAllMovies , zaczęliśmy od zaznaczenia wszystkich dokumentów typu movie , ale poszliśmy dalej, aby wybrać tylko te, których identyfikator został podany do funkcji. Ponieważ zamierzamy wyświetlić wiele szczegółów filmu, określiliśmy kilka atrybutów do pobrania.

Pobraliśmy id filmu, a także kilka atrybutów z tablicy castMembers , a mianowicie ref , characterName , imię osoby i jej obraz. Zmieniliśmy również alias z castMembers na cast .

Podobnie jak castMembers , wybraliśmy kilka atrybutów z tablicy crewMembers , a mianowicie ref , department , job , imię osoby i jej wizerunek. zmieniliśmy również alias z crewMembers na crew .

W ten sam sposób wybraliśmy tekst poglądowy, popularność, adres URL plakatu filmu, datę premiery i tytuł filmu.

Język GROQ firmy Sanity pozwala nam również na sortowanie dokumentów. Aby posortować element, przekazujemy kolejność obok operatora rury .

Na przykład, jeśli chcemy posortować filmy według ich releaseDate w porządku rosnącym, możemy wykonać następujące czynności.

 const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );

Użyliśmy tego pojęcia w funkcji sortMoviesBy do sortowania według kolejności rosnącej lub malejącej.

Przyjrzyjmy się tej funkcji poniżej.

 const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };

Zaczęliśmy od wywołania akcji typu MOVIES_SORT_REQUEST w celu określenia, kiedy żądanie jest ładowane. Następnie użyliśmy składni GROQ do sortowania i pobierania danych z kolekcji movie . Pozycja według której ma być sortowana podawana jest w item zmiennej, a tryb sortowania (rosnąco lub malejąco) jest podawana w zmiennej type . W związku z tym zwróciliśmy id , adres URL plakatu i tytuł. Gdy dane zostaną zwrócone, wywołaliśmy akcję typu MOVIES_SORT_SUCCESS , a jeśli się nie powiedzie, wywołamy akcję typu MOVIES_SORT_FAIL .

Podobna koncepcja GROQ dotyczy funkcji getMostPopular . Składnia GROQ jest pokazana poniżej.

 const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );

Jedyna różnica polega na tym, że posortowaliśmy filmy według popularności w porządku malejącym, a następnie wybraliśmy tylko trzy pierwsze. Pozycje są zwracane w indeksie liczonym od zera, więc pierwsze trzy pozycje to pozycje 0, 1 i 2. Jeśli chcemy pobrać pierwsze dziesięć pozycji, możemy przekazać [0..9] do funkcji.

Oto pełny kod akcji filmu w pliku movieActions.js .

 import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };

Konfigurowanie reduktorów

Reduktory to jedna z najważniejszych koncepcji w Redux. Biorą poprzedni stan i określają zmiany stanu.

Zazwyczaj będziemy używać instrukcji switch do wykonania warunku dla każdego typu akcji. Na przykład możemy zwrócić loading , gdy typ akcji oznacza ładowanie, a następnie ładunek, gdy oznacza sukces lub błąd. Oczekuje się, że jako argumenty przyjmie initial state i action .

Nasz plik movieReducers.js zawiera różne reduktory pasujące do akcji zdefiniowanych w pliku movieActions.js . Jednak każdy z reduktorów ma podobną składnię i strukturę. Jedyne różnice to wywołane przez nie constants i wartości, które zwracają.

Zacznijmy od przyjrzenia się fetchAllMoviesReducer w pliku movieReducers.js .

 import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };

Podobnie jak wszystkie reduktory, fetchAllMoviesReducer przyjmuje jako argumenty obiekt stanu początkowego ( state ) i obiekt action . Użyliśmy instrukcji switch, aby sprawdzić typy akcji w każdym momencie. Jeśli odpowiada to MOVIES_FETCH_REQUEST , zwracamy ładowanie jako true, aby umożliwić nam pokazanie użytkownikowi wskaźnika ładowania.

Jeśli odpowiada to MOVIES_FETCH_SUCCESS , wyłączamy wskaźnik ładowania, a następnie zwracamy ładunek akcji w zmiennych movies . Ale jeśli jest to MOVIES_FETCH_FAIL , wyłączamy również ładowanie, a następnie zwracamy błąd. Chcemy również mieć możliwość zresetowania naszych filmów. Umożliwi nam to wyczyszczenie stanów, gdy zajdzie taka potrzeba.

Taką samą konstrukcję mamy dla innych reduktorów. Kompletny movieReducers.js jest pokazany poniżej.

 import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };

Postępowaliśmy również zgodnie z dokładnie taką samą strukturą dla personReducers.js . Na przykład funkcja fetchAllPersonsReducer definiuje stany pobierania wszystkich osób z bazy danych.

Jest to podane w poniższym kodzie.

 import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };

Podobnie jak fetchAllMoviesReducer , zdefiniowaliśmy fetchAllPersonsReducer ze state i action jako argumentami. Są to standardowe ustawienia dla reduktorów Redux. Następnie użyliśmy instrukcji switch do sprawdzenia typów akcji i jeśli jest to typ PERSONS_FETCH_REQUEST , zwracamy ładowanie jako true. Jeśli jest to PERSONS_FETCH_SUCCESS , wyłączamy ładowanie i zwracamy ładunek, a jeśli PERSONS_FETCH_FAIL , zwracamy błąd.

Łączenie Reduktorów

Funkcja combineReducers w Redux pozwala nam połączyć więcej niż jeden reduktor i przekazać go do sklepu. Połączymy nasze reduktory filmów i osób w index.js w folderze reducers .

Przyjrzyjmy się temu.

 import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });

Tutaj zaimportowaliśmy wszystkie reduktory z pliku filmów, osób i globalnych reduktorów i przekazaliśmy je do funkcji combineReducers . Funkcja combineReducers pobiera obiekt, który pozwala nam przejść wszystkie nasze reduktory. Możemy nawet dodać alias do argumentów w procesie.

Na globalReducers później.

Możemy teraz przekazać reduktory w pliku Redux store.js . Jest to pokazane poniżej.

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));

Po skonfigurowaniu naszego przepływu pracy Redux, skonfigurujmy naszą aplikację React.

Konfigurowanie naszej aplikacji React

Nasza aplikacja React wyświetli listę filmów i odpowiadających im członków obsady i załogi. Będziemy używać react-router-dom do routingu i styled-components do stylizacji aplikacji. Użyjemy również Material UI dla ikon i niektórych komponentów UI.

Wprowadź następujące polecenie bash , aby zainstalować zależności.

 npm install react-router-dom @material-ui/core @material-ui/icons query-string

Oto, co będziemy budować:

Podłączanie Redux do naszej aplikacji React

React-redux dostarczany z funkcją Provider , która pozwala nam połączyć naszą aplikację ze sklepem Redux. W tym celu musimy przekazać Usługodawcy instancję sklepu. Możemy to zrobić w naszym index.js lub App.js

Oto nasz plik index.js.

 import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );

Tutaj zaimportowaliśmy Provider z react-redux i store z naszego sklepu Redux. Następnie owinęliśmy całe drzewo komponentów dostawcą, przekazując do niego sklep.

Następnie potrzebujemy React react-router-dom do routingu w naszej aplikacji React. React react-router-dom zawiera BrowserRouter , Switch i Route , których można użyć do zdefiniowania naszej ścieżki i tras.

Robimy to w naszym pliku App.js Jest to pokazane poniżej.

 import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;

Jest to standardowa konfiguracja dla routingu z reakcją-router-dom. Możesz to sprawdzić w ich dokumentacji. Zaimportowaliśmy nasze komponenty Header , Footer , PersonsList i MovieList . Następnie konfigurujemy reakcję- Router react-router-dom , umieszczając wszystko w routerach i Switch .

Ponieważ chcemy, aby nasze strony współdzieliły ten sam nagłówek i stopkę, musieliśmy przekazać komponenty <Header /> i <Footer /> przed zawinięciem struktury za pomocą Switch . Podobnie zrobiliśmy z main elementem, ponieważ chcemy, aby otaczał całą aplikację.

Przekazaliśmy każdy składnik do trasy za pomocą Route z react-router-dom .

Definiowanie naszych stron i komponentów

Nasza aplikacja jest zorganizowana w uporządkowany sposób. Komponenty wielokrotnego użytku są przechowywane w folderze components , podczas gdy strony są przechowywane w folderze pages .

Nasze pages zawierają movieListPage.js , moviePage.js , PersonListPage.js i PersonPage.js . MovieListPage.js zawiera listę wszystkich filmów w naszym zapleczu Sanity.io, a także najpopularniejsze filmy.

Aby wyświetlić listę wszystkich filmów, po prostu fetchAllMovies dispatch w naszym pliku movieAction.js . Ponieważ musimy pobrać listę zaraz po załadowaniu strony, musimy ją zdefiniować w useEffect . Jest to pokazane poniżej.

 import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;

Dzięki useDispatch i useSelector możemy wysyłać akcje Redux i wybierać odpowiednie stany ze sklepu Redux. Zwróć uwagę, że stany loading , error i movies zostały zdefiniowane w naszych funkcjach Reducer i tutaj zostały wybrane za pomocą useSelector z React Redux. Te stany, a mianowicie loading , error i movies , stają się dostępne natychmiast po uruchomieniu akcji fetchAllMovies() .

Gdy już otrzymamy listę filmów, możemy wyświetlić ją w naszej aplikacji za pomocą funkcji map lub w dowolny inny sposób.

Oto pełny kod pliku moviesListPage.js .

import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage

Zaczęliśmy od wysłania akcji getMostPopular movies (ta akcja wybiera filmy o największej popularności) w useEffect . Dzięki temu możemy pobrać najpopularniejsze filmy zaraz po załadowaniu strony. Dodatkowo umożliwiliśmy użytkownikom sortowanie filmów według releaseDate i popularity . Jest to obsługiwane przez akcję sortMoviesBy w powyższym kodzie. Ponadto fetchAllMovies w zależności od parametrów zapytania.

Ponadto użyliśmy useSelector do wybrania odpowiednich reduktorów dla każdej z tych akcji. Dla każdego z reduktorów wybraliśmy stany loading , error i movies .

Po pobraniu movies z reduktorów możemy teraz wyświetlić je użytkownikowi. Tutaj wykorzystaliśmy do tego funkcję map ES6. Najpierw wyświetlaliśmy program ładujący za każdym razem, gdy ładowany jest każdy ze stanów filmu, a jeśli wystąpi błąd, wyświetlamy komunikat o błędzie. Wreszcie, jeśli otrzymamy film, wyświetlamy obraz filmu użytkownikowi za pomocą funkcji map . Całość otoczyliśmy składnikiem MovieListContainer .

Znacznik <MovieListContainer> … </MovieListContainer> to element div zdefiniowany przy użyciu stylizowanych komponentów. Wkrótce się temu przyjrzymy.

Stylizowanie naszej aplikacji za pomocą stylizowanych komponentów

Styled components pozwalają nam na indywidualne stylizowanie naszych stron i komponentów. Oferuje również kilka interesujących funkcji, takich jak inheritance , Theming , passing of props itp.

Chociaż zawsze chcemy stylizować nasze strony indywidualnie, czasami stylizacja globalna może być pożądana. Co ciekawe, styled-components umożliwiają to dzięki funkcji createGlobalStyle .

Aby używać styled-components w naszej aplikacji, musimy ją zainstalować. Otwórz terminal w projekcie React i wprowadź następujące polecenie bash .

 npm install styled-components

Po zainstalowaniu styled-components, zacznijmy od naszych globalnych stylów.

Stwórzmy osobny folder w naszym katalogu src o nazwie styles . To przechowa wszystkie nasze style. Utwórzmy również plik globalStyles.js w folderze style. Aby stworzyć styl globalny w styled-components, musimy zaimportować createGlobalStyle .

 import { createGlobalStyle } from "styled-components";

Następnie możemy zdefiniować nasze style w następujący sposób:

 export const GlobalStyle = createGlobalStyle` ... `

Stylizowane komponenty wykorzystują literał szablonu do definiowania właściwości. W ramach tego literału możemy pisać nasze tradycyjne kody CSS .

Zaimportowaliśmy również deviceWidth zdefiniowany w pliku o nazwie definition.js . deviceWidth zawiera definicję punktów przerwania do ustawiania naszych zapytań o media.

 import { deviceWidth } from "./definition";

Ustawiliśmy overflow na ukryty, aby kontrolować przepływ naszej aplikacji.

 html, body{ overflow-x: hidden; }

Zdefiniowaliśmy również styl nagłówka za pomocą selektora stylu .header .

 .header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }

Tutaj zdefiniowane są różne style, takie jak kolor tła, indeks Z, dopełnienie i wiele innych tradycyjnych właściwości CSS.

Użyliśmy props styled-components, aby ustawić kolor tła. To pozwala nam ustawić zmienne dynamiczne, które mogą być przekazywane z naszego komponentu. Co więcej, przekazaliśmy również zmienną motywu, aby umożliwić nam maksymalne wykorzystanie możliwości przełączania motywów.

Tworzenie motywów jest tutaj możliwe, ponieważ całą naszą aplikację otoczyliśmy narzędziem ThemeProvider ze styled-components. Porozmawiamy o tym za chwilę. Co więcej, użyliśmy CSS flexbox , aby odpowiednio wystylizować nasz nagłówek i ustawić pozycję na fixed , aby upewnić się, że pozostaje stała w stosunku do przeglądarki. Zdefiniowaliśmy również punkty przerwania, aby nagłówki były przyjazne dla urządzeń mobilnych.

Oto pełny kod naszego pliku globalStyles.js .

 import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;

Zauważ, że napisaliśmy czysty kod CSS w obrębie literału, ale jest kilka wyjątków. Styled-components pozwala nam przekazywać rekwizyty. Możesz dowiedzieć się więcej na ten temat w dokumentacji.

Oprócz definiowania stylów globalnych możemy zdefiniować style dla poszczególnych stron.

Na przykład, oto styl dla PersonListPage.js zdefiniowany w PersonStyle.js w folderze styles .

 import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;

Najpierw zaimportowaliśmy styled z styled-components i deviceWidth z pliku definition . Następnie zdefiniowaliśmy PersonsListContainer jako div do przechowywania naszych stylów. Korzystając z zapytań o media i ustalonych punktów przerwania, dostosowaliśmy stronę do urządzeń mobilnych, ustawiając różne punkty przerwania.

Tutaj użyliśmy tylko standardowych punktów przerwania przeglądarki dla małych, dużych i bardzo dużych ekranów. W pełni wykorzystaliśmy również flexbox i siatkę CSS, aby odpowiednio stylizować i wyświetlać nasze treści na stronie.

Aby użyć tego stylu w naszym pliku PersonListPage.js , po prostu zaimportowaliśmy go i dodaliśmy do naszej strony w następujący sposób.

 import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;

Opakowanie wyświetli div , ponieważ zdefiniowaliśmy go jako div w naszych stylach.

Dodawanie motywów i pakowanie

Dodawanie motywów do naszej aplikacji to zawsze fajna funkcja. W tym celu potrzebujemy:

  • Nasze niestandardowe motywy zdefiniowane w osobnym pliku (w naszym przypadku definition.js ).
  • Logika zdefiniowana w naszych akcjach Redux i reduktorach.
  • Wywołanie naszego motywu w naszej aplikacji i przekazanie go przez drzewo komponentów.

Sprawdźmy to.

Oto nasz obiekt theme w pliku definition.js .

 export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };

Do jasnych i ciemnych motywów dodaliśmy różne właściwości kolorystyczne. Kolory są starannie dobrane, aby zapewnić widoczność zarówno w trybie jasnym, jak i ciemnym. Możesz zdefiniować swoje motywy, jak chcesz. To nie jest twarda i szybka zasada.

Następnie dodajmy funkcjonalność do Redux.

Stworzyliśmy globalActions.js w naszym folderze akcji Redux i dodaliśmy następujące kody.

 import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };

Tutaj po prostu zaimportowaliśmy nasze zdefiniowane motywy. Wysłano odpowiednie działania, przekazując ładunek motywów, których potrzebowaliśmy. Wyniki ładunku są przechowywane w pamięci lokalnej przy użyciu tych samych kluczy zarówno dla jasnych, jak i ciemnych motywów. Dzięki temu możemy zachować stany w przeglądarce.

Musimy również zdefiniować nasz reduktor dla tematów.

 import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };

To jest bardzo podobne do tego, co robiliśmy. Użyliśmy instrukcji switch , aby sprawdzić typ akcji, a następnie zwróciliśmy odpowiedni payload . Zwróciliśmy również light stanu, które określa, czy użytkownik wybrał jasny, czy ciemny motyw. Wykorzystamy to w naszych komponentach.

Musimy go również dodać do naszego reduktora i sklepu. Oto pełny kod naszego store.js .

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));

Ponieważ musieliśmy zachować motyw podczas odświeżania przez użytkownika, musieliśmy pobrać go z lokalnego magazynu za pomocą localStorage.getItem() i przekazać go do naszego stanu początkowego.

Dodawanie funkcjonalności do naszej aplikacji React

Styled Components dostarczają nam ThemeProvider , który pozwala nam przekazywać motywy przez naszą aplikację. Możemy zmodyfikować nasz plik App.js, aby dodać tę funkcjonalność.

Przyjrzyjmy się temu.

 import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;

Przekazując motywy przez ThemeProvider , możemy z łatwością używać rekwizytów motywów w naszych stylach.

Na przykład możemy ustawić kolor na nasz niestandardowy kolor bodyText w następujący sposób.

 color: ${(props) => props.theme.bodyText};

Motywów niestandardowych możemy używać wszędzie tam, gdzie potrzebujemy koloru w naszej aplikacji.

Na przykład, aby zdefiniować border-bottom , robimy co następuje.

 border-bottom: 2px solid ${(props) => props.theme.goldish};

Wniosek

Zaczęliśmy od zagłębienia się w Sanity.io, skonfigurowania go i połączenia z naszą aplikacją React. Następnie skonfigurowaliśmy Redux i użyliśmy języka GROQ do zapytania naszego API. Zobaczyliśmy, jak połączyć i używać Redux z naszą aplikacją React za pomocą react-redux , używać stylizowanych komponentów i motywów.

Jednak tylko zarysowaliśmy powierzchnię tego, co jest możliwe dzięki tym technologiom. Zachęcam do przejrzenia próbek kodu w moim repozytorium GitHub i spróbowania swoich sił w zupełnie innym projekcie wykorzystującym te technologie do ich nauki i opanowania.

Zasoby

  • Dokumentacja poczytalności
  • Jak zbudować bloga z Sanity.io autorstwa Kapehe
  • Dokumentacja redukcyjna
  • Dokumentacja stylizowanych komponentów
  • Ściągawka GROQ
  • Materiałowa dokumentacja interfejsu użytkownika
  • Redux Middleware i efekty uboczne
  • Dokumentacja Redux Thunk