Создание веб-приложения с помощью React, Redux и Sanity.io

Опубликовано: 2022-03-10
Краткое резюме ↬ Headless CMS — это мощный и простой способ управления контентом и доступа к API. Sanity.io, основанный на React, представляет собой удобный инструмент для гибкого управления контентом. Его можно использовать для создания простых и сложных приложений с нуля. В этой статье Ифеани объясняет, как создать простое приложение для листинга с помощью Sanity.io и React. Глобальные состояния будут управляться с помощью Redux, а приложение будет оформлено с помощью стилевых компонентов.

Быстрая эволюция цифровых платформ наложила серьезные ограничения на традиционные CMS, такие как Wordpress. Эти платформы связаны, негибки и ориентированы на проект, а не на продукт. К счастью, для решения этих и многих других проблем было разработано несколько безголовых CMS.

В отличие от традиционных CMS, безголовые CMS, которые можно описать как «программное обеспечение как услуга» (SaaS), можно использовать для разработки веб-сайтов, мобильных приложений, цифровых дисплеев и многого другого. Их можно использовать на неограниченном количестве платформ. Если вы ищете CMS, независимую от платформы, предназначенную в первую очередь для разработчиков и предлагающую межплатформенную поддержку, вам не нужно искать что-то далекое от безголовой CMS.

Безголовая CMS — это просто CMS без головы. head здесь относится к внешнему интерфейсу или уровню представления, а body относится к серверной части или репозиторию контента. Это дает много интересных преимуществ. Например, это позволяет разработчику выбрать любой внешний интерфейс по своему выбору, и вы также можете спроектировать уровень представления по своему усмотрению.

Существует множество безголовых CMS, некоторые из самых популярных включают Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus и т. д. Эти безголовые CMS основаны на API и имеют свои сильные стороны. Например, такие CMS, как Sanity, Strapi, Contentful и Storyblok, бесплатны для небольших проектов.

Эти безголовые CMS также основаны на разных технологических стеках. В то время как Sanity.io основан на React.js, Storyblok основан на Vue.js. Как разработчик React, это основная причина, по которой я быстро заинтересовался Sanity. Однако, будучи безголовой CMS, каждая из этих платформ может быть подключена к любому интерфейсу, будь то Angular, Vue или React.

Каждая из этих безголовых CMS имеет как бесплатные, так и платные планы, которые представляют собой значительный скачок цен. Хотя эти платные планы предлагают больше функций, вы не хотели бы платить столько за проект малого и среднего размера. Sanity пытается решить эту проблему, вводя варианты оплаты по мере использования. С помощью этих опций вы сможете платить за то, что используете, и избежать скачка цен.

Еще одна причина, по которой я выбираю Sanity.io, — это их язык GROQ. Для меня Sanity выделяется из толпы, предлагая этот инструмент. Графически-реляционные объектные запросы (GROQ) сокращают время разработки, помогают получить нужный контент в нужном вам виде, а также помогают разработчику создать документ с новой моделью контента без изменения кода.

Более того, разработчики не ограничены языком GROQ. Вы также можете использовать GraphQL или даже традиционные axios и fetch в своем приложении React для запроса бэкэнда. Как и большинство других безголовых CMS, Sanity имеет исчерпывающую документацию, содержащую полезные советы по созданию платформы.

Примечание. Эта статья требует базового понимания React, Redux и CSS.

Еще после прыжка! Продолжить чтение ниже ↓

Начало работы с Sanity.io

Чтобы использовать Sanity на вашем компьютере, вам необходимо установить инструмент Sanity CLI. Хотя это можно установить локально в вашем проекте, предпочтительнее установить его глобально, чтобы сделать его доступным для любых будущих приложений.

Для этого введите в терминале следующие команды.

 npm install -g @sanity/cli

Флаг -g в приведенной выше команде включает глобальную установку.

Далее нам нужно инициализировать Sanity в нашем приложении. Хотя это можно установить как отдельный проект, обычно предпочтительнее установить его в вашем внешнем приложении (в данном случае React).

В своем блоге Капехе подробно объяснила, как интегрировать Sanity с React. Будет полезно ознакомиться со статьей, прежде чем продолжить работу с этим учебным пособием.

Введите следующие команды, чтобы инициализировать Sanity в вашем приложении React.

 sanity init

Команда sanity становится доступной нам, когда мы установили инструмент Sanity CLI. Вы можете просмотреть список доступных команд Sanity, набрав sanity или sanity help в своем терминале.

При настройке или инициализации вашего проекта вам нужно будет следовать инструкциям, чтобы настроить его. Вам также потребуется создать набор данных, и вы даже можете выбрать их собственный набор данных, заполненный данными. Для этого приложения для листинга мы будем использовать пользовательский набор данных о научно-фантастических фильмах Sanity. Это избавит нас от необходимости вводить данные самостоятельно.

Чтобы просмотреть и отредактировать набор данных, cd в подкаталог Sanity в своем терминале и введите sanity start . Обычно это работает на https://localhost:3333/ . Вам может потребоваться войти в систему для доступа к интерфейсу (убедитесь, что вы входите с той же учетной записью, которую вы использовали при инициализации проекта). Скриншот окружения показан ниже.

Обзор сервера Sanity
Обзор сервера здравомыслия для набора данных научно-фантастических фильмов. (Большой превью)

Двусторонняя связь Sanity-React

Sanity и React должны взаимодействовать друг с другом для полнофункционального приложения.

Настройка CORS Origins в Sanity Manager

Сначала мы подключим наше приложение React к Sanity. Для этого войдите на https://manage.sanity.io/ и найдите CORS origins в разделе « API Settings » на вкладке « Settings ». Здесь вам нужно привязать источник внешнего интерфейса к серверной части Sanity. Наше приложение React по умолчанию работает на https://localhost:3000/ , поэтому нам нужно добавить его в CORS.

Это показано на рисунке ниже.

Настройки источника CORS
Установка источника CORS в Sanity.io Manager. (Большой превью)

Подключение здравомыслия к реакции

Sanity связывает project ID с каждым проектом, который вы создаете. Этот идентификатор необходим при подключении к вашему внешнему приложению. Вы можете найти идентификатор проекта в Sanity Manager.

Бэкенд взаимодействует с React с помощью библиотеки, известной как sanity client . Вам необходимо установить эту библиотеку в свой проект Sanity, введя следующие команды.

 npm install @sanity/client

Создайте файл sanitySetup.js (имя файла не имеет значения) в папке src вашего проекта и введите следующие коды React, чтобы установить соединение между Sanity и React.

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

Мы передали наш projectId , dataset name и логическое значение useCdn экземпляру вменяемого клиента, импортированному из @sanity/client . Это работает волшебно и соединяет наше приложение с серверной частью.

Теперь, когда мы завершили двустороннее соединение, давайте приступим к созданию нашего проекта.

Настройка и подключение Redux к нашему приложению

Нам понадобится несколько зависимостей для работы с Redux в нашем приложении React. Откройте свой терминал в среде React и введите следующие команды bash.

 npm install redux react-redux redux-thunk

Redux — это глобальная библиотека управления состоянием, которую можно использовать с большинством интерфейсных фреймворков и библиотек, таких как React. Однако нам нужен промежуточный инструмент react-redux , чтобы обеспечить связь между нашим магазином Redux и нашим приложением React. Преобразователь Redux поможет нам вернуть функцию вместо объекта действия из Redux.

Хотя мы могли бы написать весь рабочий процесс Redux в одном файле, зачастую проще и лучше разделить наши задачи. Для этого мы разделим наш рабочий процесс на три файла, а именно: actions , reducers и store . Однако нам также нужен отдельный файл для хранения action types , также известных как constants .

Настройка магазина

Магазин — самый важный файл в Redux. Он организует и упаковывает состояния и отправляет их в наше приложение React.

Вот первоначальная настройка нашего магазина Redux, необходимая для подключения нашего рабочего процесса Redux.

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

Функция createStore в этом файле принимает три параметра: reducer (обязательный), начальное состояние и энхансер (обычно промежуточное ПО, в данном случае applyMiddleware thunk . Наши редукторы будут храниться в папке reducers , и мы будем объединять и экспортировать их в файл index.js в папке reducers . Это файл, который мы импортировали в приведенном выше коде. Мы вернемся к этому файлу позже.

Введение в язык GROQ Sanity

Sanity продвигает запросы к данным JSON на шаг вперед, представляя GROQ. GROQ расшифровывается как Graph-Relational Object Queries. Согласно Sanity.io, GROQ — это декларативный язык запросов, предназначенный для запросов к коллекциям документов JSON, в основном не имеющих схемы.

Sanity даже предоставляет игровую площадку GROQ , чтобы помочь разработчикам ознакомиться с языком. Однако, чтобы получить доступ к игровой площадке, вам необходимо установить здравомыслие . Запустите sanity install @sanity/vision на своем терминале, чтобы установить его.

GROQ имеет синтаксис, аналогичный GraphQL, но более компактный и удобный для чтения. Кроме того, в отличие от GraphQL, GROQ можно использовать для запроса данных JSON.

Например, чтобы получить каждый элемент в нашем документе фильма, мы будем использовать следующий синтаксис GROQ.

 *[_type == "movie"]

Однако, если мы хотим получить только _ids и crewMembers в нашем документе фильма. Нам нужно указать эти поля следующим образом.

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

Здесь мы использовали * , чтобы сообщить GROQ, что нам нужен каждый документ фильма _type . _type — это атрибут коллекции фильмов. Мы также можем вернуть тип, как мы сделали это _id и crewMembers , следующим образом:

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

Мы будем больше работать над GROQ, реализуя его в наших действиях Redux, но вы можете проверить документацию Sanity.io для GROQ, чтобы узнать больше об этом. Шпаргалка по запросам GROQ содержит множество примеров, которые помогут вам освоить язык запросов.

Настройка констант

Нам нужны константы для отслеживания типов действий на каждом этапе рабочего процесса Redux. Константы помогают определить тип действия, отправляемого в каждый момент времени. Например, мы можем отслеживать, когда API загружается, полностью загружается и когда возникает ошибка.

Нам не обязательно определять константы в отдельном файле, но для простоты и ясности это обычно лучшая практика в Redux.

По соглашению константы в Javascript определяются в верхнем регистре. Мы будем следовать рекомендациям здесь, чтобы определить наши константы. Вот пример константы для обозначения запросов на получение движущегося фильма.

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";

Здесь мы создали константу MOVIE_FETCH_REQUEST , которая обозначает тип действия MOVIE_FETCH_REQUEST . Это помогает нам легко вызывать этот тип действия без использования strings и избегать ошибок. Мы также экспортировали константу, чтобы она была доступна в любом месте нашего проекта.

Точно так же мы можем создать другие константы для получения типов действий, обозначающих, когда запрос успешен или нет. Полный код для movieConstants.js приведен в приведенном ниже коде.

 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";

Здесь мы определили несколько констант для выборки фильма или списка фильмов, сортировки и выборки самых популярных фильмов. Обратите внимание, что мы устанавливаем константы, чтобы определить, когда запрос loading , successful и failed .

Точно так же наш файл personConstants.js приведен ниже:

 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";

Как и в случае с movieConstants.js , мы устанавливаем список констант для выборки человека или людей. Мы также устанавливаем константу для подсчета лиц. Константы соответствуют соглашению, описанному для movieConstants.js , и мы также экспортировали их, чтобы они были доступны для других частей нашего приложения.

Наконец, мы реализуем светлый и темный режимы в приложении, так что у нас есть еще один файл констант globalConstants.js . Давайте посмотрим на это.

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

Здесь мы устанавливаем константы, чтобы определить, когда отправляется светлый или темный режим. SET_LIGHT_THEME определяет, когда пользователь переключается на светлую тему, а SET_DARK_THEME определяет, когда выбирается темная тема. Мы также экспортировали наши константы, как показано.

Настройка действий

По соглашению наши действия хранятся в отдельной папке. Действия сгруппированы по их типам. Например, наши действия с фильмами хранятся в movieActions.js , а наши действия с людьми хранятся в файле personActions.js .

У нас также есть globalActions.js , который позаботится о переключении темы из светлого в темный режим.

Давайте получим все фильмы 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 }); } };

Помните, мы создали файл sanitySetup.js для подключения React к нашему бэкенду Sanity? Здесь мы импортировали настройку, чтобы мы могли запрашивать наш сервер работоспособности с помощью GROQ. Мы также импортировали несколько констант, экспортированных из файла movieConstants.js в папку с constants .

Затем мы создали функцию действия fetchAllMovies для получения каждого фильма из нашей коллекции. Большинство традиционных приложений React используют axios или fetch для извлечения данных из серверной части. Но хотя здесь мы могли бы использовать любой из них, мы используем GROQ от Sanity. Чтобы войти в режим GROQ , нам нужно вызвать sanityAPI.fetch() , как показано в приведенном выше коде. Здесь sanityAPI — это соединение React-Sanity, которое мы установили ранее. Это возвращает Promise , поэтому его нужно вызывать асинхронно. Здесь мы использовали синтаксис async-await , но мы также можем использовать синтаксис .then .

Поскольку мы используем thunk в нашем приложении, мы можем вернуть функцию вместо объекта действия. Однако мы решили передать оператор return одной строкой.

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

Обратите внимание, что мы также можем написать функцию следующим образом:

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

В общем, чтобы получить все фильмы, мы сначала отправили тип действия, который отслеживает, когда запрос еще загружается. Затем мы использовали синтаксис Sanity GROQ для асинхронного запроса документа фильма. Мы _id и URL постера данных фильма. Затем мы вернули полезную нагрузку, содержащую данные, полученные от API.

Точно так же мы можем получать фильмы по их _id , сортировать фильмы и получать самые популярные фильмы.

Мы также можем получить фильмы, соответствующие референсу конкретного человека. Мы сделали это в функции 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 }); } };

Эта функция принимает аргумент и проверяет, соответствует ли person._ref либо в castMembers , либо в crewMembers переданному аргументу. Мы возвращаем фильм _id , poster url и title вместе. Мы также отправляем действие типа MOVIES_REF_FETCH_SUCCESS , прикрепляя полезную нагрузку возвращаемых данных, и, если возникает ошибка, мы отправляем действие типа MOVIE_REF_FETCH_FAIL , прикрепляя полезную нагрузку сообщения об ошибке, благодаря оболочке try-catch .

В функции fetchMovieById мы использовали GROQ для извлечения фильма, который соответствует определенному id , переданному в функцию.

Синтаксис GROQ для функции показан ниже.

 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]` );

Как и в случае с действием fetchAllMovies , мы начали с выбора всех документов типа movie , но пошли дальше и выбрали только те, идентификатор которых предоставлен функции. Поскольку мы намерены отображать множество деталей фильма, мы указали набор атрибутов для извлечения.

Мы получили id фильма, а также несколько атрибутов в массиве castMembers , а именно ref , characterName , имя человека и изображение человека. Мы также изменили псевдоним с castMembers на cast .

Как и castMembers , мы выбрали несколько атрибутов из массива crewMembers , а именно ref , department , job , имя человека и изображение человека. мы также изменили псевдоним с crewMembers членов экипажа на crew .

Точно так же мы выбрали текст обзора, популярность, ссылку на постеры фильма, дату выхода фильма и название.

Язык Sanity GROQ также позволяет нам сортировать документ. Чтобы отсортировать элемент, мы передаем порядок рядом с оператором вертикальной черты.

Например, если мы хотим отсортировать фильмы по releaseDate в порядке возрастания, мы можем сделать следующее.

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

Мы использовали это понятие в функции sortMoviesBy для сортировки по возрастанию или по убыванию.

Рассмотрим эту функцию ниже.

 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 }); } };

Мы начали с отправки действия типа MOVIES_SORT_REQUEST , чтобы определить, когда загружается запрос. Затем мы использовали синтаксис GROQ для сортировки и извлечения данных из коллекции movie . Элемент для сортировки указывается в переменной item а режим сортировки (по возрастанию или по убыванию) — в переменной type . Следовательно, мы вернули id , URL постера и заголовок. Как только данные возвращены, мы отправляем действие типа MOVIES_SORT_SUCCESS , а в случае сбоя мы отправляем действие типа MOVIES_SORT_FAIL .

Аналогичная концепция GROQ применяется к функции getMostPopular . Синтаксис GROQ показан ниже.

 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]` );

Разница лишь в том, что мы отсортировали фильмы по популярности в порядке убывания, а затем выбрали только первые три. Элементы возвращаются в индексе, начинающемся с нуля, поэтому первые три элемента — это элементы 0, 1 и 2. Если мы хотим получить первые десять элементов, мы можем передать [0..9] .

Вот полный код действий фильма в файле 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 };

Настройка редукторов

Редюсеры — одна из самых важных концепций Redux. Они принимают предыдущее состояние и определяют изменения состояния.

Как правило, мы будем использовать оператор switch для выполнения условия для каждого типа действия. Например, мы можем вернуть loading , когда тип действия обозначает загрузку, а затем полезную нагрузку, когда она обозначает успех или ошибку. Ожидается, что в качестве аргументов он будет принимать initial state и action .

Наш файл movieReducers.js содержит различные редюсеры, соответствующие действиям, определенным в файле movieActions.js . Однако каждый из редукторов имеет схожий синтаксис и структуру. Единственные различия — это constants , которые они вызывают, и значения, которые они возвращают.

Давайте начнем с просмотра fetchAllMoviesReducer в файле 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; } };

Как и все редюсеры, fetchAllMoviesReducer принимает в качестве аргументов объект начального состояния ( state ) и объект action . Мы использовали оператор switch для проверки типов действий в каждый момент времени. Если это соответствует MOVIES_FETCH_REQUEST , мы возвращаем загрузку как истину, чтобы позволить нам показать пользователю индикатор загрузки.

Если он соответствует MOVIES_FETCH_SUCCESS , мы отключаем индикатор загрузки и затем возвращаем полезную нагрузку действия в переменной movies . Но если это MOVIES_FETCH_FAIL , мы тоже отключаем загрузку и потом возвращаем ошибку. Нам также нужна возможность сбросить наши фильмы. Это позволит нам очищать состояния, когда нам это нужно.

У нас такая же структура для других редукторов. Полный movieReducers.js показан ниже.

 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 };

Мы также следовали той же самой структуре для personReducers.js . Например, функция fetchAllPersonsReducer определяет состояния для выборки всех людей в базе данных.

Это указано в приведенном ниже коде.

 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; } };

Как и fetchAllMoviesReducer , мы определили fetchAllPersonsReducer с state и action в качестве аргументов. Это стандартная настройка для редукторов Redux. Затем мы использовали оператор switch для проверки типов действий, и если он имеет тип PERSONS_FETCH_REQUEST , мы возвращаем загрузку как истину. Если это PERSONS_FETCH_SUCCESS , мы отключаем загрузку и возвращаем полезную нагрузку, а если это PERSONS_FETCH_FAIL , мы возвращаем ошибку.

Объединение редукторов

Функция combineReducers Redux позволяет нам комбинировать более одного редюсера и передавать его в хранилище. Мы объединим наши редукторы фильмов и людей в файле index.js в папке reducers .

Давайте посмотрим на это.

 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 });

Здесь мы импортировали все редукторы из файла фильмов, людей и глобальных редукторов и передали их функции combineReducers . Функция combineReducers принимает объект, который позволяет нам передавать все наши редукторы. Мы даже можем добавить псевдоним к аргументам в процессе.

Мы поработаем над globalReducers позже.

Теперь мы можем передать редукторы в файле Redux store.js . Это показано ниже.

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

Настроив рабочий процесс Redux, давайте настроим наше приложение React.

Настройка нашего приложения React

Наше реагирующее приложение будет перечислять фильмы и соответствующие им актеры и членов съемочной группы. Мы будем использовать react-router-dom для маршрутизации и styled-components для стилизации приложения. Мы также будем использовать Material UI для иконок и некоторых компонентов пользовательского интерфейса.

Введите следующую команду bash , чтобы установить зависимости.

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

Вот что мы будем строить:

Подключение Redux к нашему приложению React

React-redux поставляется с функцией Provider , которая позволяет нам подключить наше приложение к хранилищу Redux. Для этого нам нужно передать экземпляр хранилища провайдеру. Мы можем сделать это либо в нашем index.js , либо в файле App.js

Вот наш файл 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") );

Здесь мы импортировали Provider из react-redux и сохранили его из нашего store Redux. Затем мы обернули все наше дерево компонентов Provider, передав ему хранилище.

Далее нам понадобится react-router-dom для маршрутизации в нашем приложении React. react react-router-dom поставляется с BrowserRouter , Switch и Route , которые можно использовать для определения нашего пути и маршрутов.

Мы делаем это в нашем файле App.js Это показано ниже.

 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;

Это стандартная настройка маршрутизации с помощью react-router-dom. Вы можете проверить это в их документации. Мы импортировали наши компоненты Header , Footer , PersonsList и MovieList . Затем мы настроили react-router-dom , завернув все в Router и Switch .

Так как мы хотим, чтобы наши страницы использовали один и тот же верхний и нижний колонтитулы, нам пришлось передать компоненты <Header /> и <Footer /> , прежде чем оборачивать структуру с помощью Switch . Мы также сделали то же самое с main элементом, так как мы хотим, чтобы он обернул все приложение.

Мы передавали каждый компонент в маршрут, используя Route из react-router-dom .

Определение наших страниц и компонентов

Наше приложение структурировано. Компоненты многократного использования хранятся в папке components , а страницы — в папке pages .

Наши pages включают movieListPage.js , moviePage.js , PersonListPage.js и PersonPage.js . На MovieListPage.js перечислены все фильмы в нашем бэкэнде Sanity.io, а также самые популярные фильмы.

Чтобы получить список всех фильмов, мы просто dispatch действие fetchAllMovies , определенное в нашем файле movieAction.js . Поскольку нам нужно получить список, как только страница загрузится, мы должны определить его в useEffect . Это показано ниже.

 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;

Благодаря useDispatch и useSelector мы можем отправлять действия Redux и выбирать соответствующие состояния из хранилища Redux. Обратите внимание, что состояния loading , error и movies были определены в наших функциях Reducer, и здесь они были выбраны с помощью useSelector из React Redux. Эти состояния, а именно loading , error и movies , становятся доступными сразу после отправки действий fetchAllMovies() .

Как только мы получим список фильмов, мы можем отобразить его в нашем приложении, используя функцию map или как угодно.

Вот полный код 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

Мы начали с отправки действия getMostPopular movie (это действие выбирает фильмы с наибольшей популярностью) в useEffect . Это позволяет нам получать самые популярные фильмы, как только страница загружается. Кроме того, мы разрешили пользователям сортировать фильмы по releaseDate и popularity . Это обрабатывается действием sortMoviesBy , отправленным в приведенном выше коде. Кроме того, мы отправили fetchAllMovies в зависимости от параметров запроса.

Кроме того, мы использовали useSelector для выбора соответствующих редюсеров для каждого из этих действий. Мы выбрали состояния для loading , error и movies для каждого из редукторов.

Получив movies из редукторов, теперь мы можем отобразить их пользователю. Здесь мы использовали для этого функцию map ES6. Сначала мы отображали загрузчик всякий раз, когда загружается каждое из состояний фильма, и в случае ошибки мы отображаем сообщение об ошибке. Наконец, если мы получаем фильм, мы отображаем изображение фильма пользователю с помощью функции map . Мы обернули весь компонент в компонент MovieListContainer .

<MovieListContainer> … </MovieListContainer> представляет собой элемент div , определенный с использованием стилизованных компонентов. Мы кратко рассмотрим это в ближайшее время.

Стилизация нашего приложения с помощью стилизованных компонентов

Стилизованные компоненты позволяют нам стилизовать наши страницы и компоненты на индивидуальной основе. Он также предлагает некоторые интересные функции, такие как inheritance , Theming , passing of props .

Хотя мы всегда хотим стилизовать наши страницы на индивидуальной основе, иногда может быть желателен глобальный стиль. Интересно, что стилизованные компоненты позволяют это сделать благодаря функции createGlobalStyle .

Чтобы использовать styled-components в нашем приложении, нам нужно его установить. Откройте свой терминал в своем проекте реагирования и введите следующую команду bash .

 npm install styled-components

Установив стилизованные компоненты, давайте начнем с наших глобальных стилей.

Давайте создадим отдельную папку в нашем каталоге src с именем styles . В нем будут храниться все наши стили. Давайте также создадим файл globalStyles.js в папке стилей. Чтобы создать глобальный стиль в styled-components, нам нужно импортировать createGlobalStyle .

 import { createGlobalStyle } from "styled-components";

Затем мы можем определить наши стили следующим образом:

 export const GlobalStyle = createGlobalStyle` ... `

Стилизованные компоненты используют литерал шаблона для определения свойств. Внутри этого литерала мы можем писать наши традиционные коды CSS .

Мы также импортировали deviceWidth определенный в файле с именем definition.js . deviceWidth содержит определение точек останова для установки наших медиа-запросов.

 import { deviceWidth } from "./definition";

Мы установили для overflow значение hidden, чтобы управлять потоком нашего приложения.

 html, body{ overflow-x: hidden; }

Мы также определили стиль заголовка с помощью селектора стиля .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%; } ... }

Здесь определяются различные стили, такие как цвет фона, z-индекс, отступы и множество других традиционных свойств CSS.

Мы использовали свойства styled- props для установки цвета фона. Это позволяет нам устанавливать динамические переменные, которые можно передать из нашего компонента. Более того, мы также передали переменную темы, чтобы максимально использовать переключение темы.

Здесь возможно создание тем, потому что мы обернули все наше приложение с помощью ThemeProvider из styled-components. Мы поговорим об этом чуть позже. Кроме того, мы использовали CSS flexbox чтобы правильно оформить наш заголовок и установить fixed позицию, чтобы убедиться, что он остается фиксированным по отношению к браузеру. Мы также определили точки останова, чтобы сделать заголовки удобными для мобильных устройств.

Вот полный код нашего файла 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}; } `;

Обратите внимание, что мы написали чистый код CSS внутри литерала, но есть несколько исключений. Styled-components позволяет нам передавать props. Подробнее об этом можно узнать в документации.

Помимо определения глобальных стилей, мы можем определить стили для отдельных страниц.

Например, вот стиль для PersonListPage.js определенный в PersonStyle.js в папке 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; } } } } `;

Сначала мы импортировали styled из styled-components и deviceWidth из файла definition . Затем мы определили PersonsListContainer как div для хранения наших стилей. Используя медиа-запросы и установленные точки останова, мы сделали страницу удобной для мобильных устройств, установив различные точки останова.

Здесь мы использовали только стандартные точки останова браузера для маленьких, больших и очень больших экранов. Мы также максимально использовали flexbox и сетку CSS для правильного оформления и отображения нашего контента на странице.

Чтобы использовать этот стиль в нашем файле PersonListPage.js , мы просто импортировали его и добавили на нашу страницу следующим образом.

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

Обертка выведет div , потому что мы определили его как div в наших стилях.

Добавление тем и завершение

Всегда полезно добавлять темы в наше приложение. Для этого нам понадобится следующее:

  • Наши пользовательские темы определены в отдельном файле (в нашем случае файле definition.js ).
  • Логика, определенная в наших действиях Redux и редюсерах.
  • Вызов нашей темы в нашем приложении и прохождение ее по дереву компонентов.

Давайте проверим это.

Вот наш объект theme в файле 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)" } };

Мы добавили различные свойства цвета для светлой и темной тем оформления. Цвета тщательно подобраны, чтобы обеспечить видимость как в светлом, так и в темном режиме. Вы можете определить свои темы, как вы хотите. Это не жесткое правило.

Далее давайте добавим функциональность в Redux.

Мы создали globalActions.js в нашей папке действий Redux и добавили следующие коды.

 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)); };

Здесь мы просто импортировали определенные темы. Отправили соответствующие действия, передав пейлоад нужных нам тем. Результаты полезной нагрузки сохраняются в локальном хранилище с использованием одних и тех же ключей как для светлой, так и для темной темы. Это позволяет нам сохранять состояния в браузере.

Нам также нужно определить наш редьюсер для тем.

 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; } };

Это очень похоже на то, что мы делали. Мы использовали оператор switch для проверки типа действия, а затем возвращали соответствующую payload . Мы также вернули light состояния, который определяет, какую тему выбирает пользователь: светлую или темную. Мы будем использовать это в наших компонентах.

Нам также нужно добавить его в наш корневой редуктор и сохранить. Вот полный код нашего 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));

Поскольку нам нужно было сохранить тему при обновлении пользователя, нам пришлось получить ее из локального хранилища с помощью localStorage.getItem() и передать ее в наше начальное состояние.

Добавление функциональности в наше приложение React

Стилизованные компоненты предоставляют нам ThemeProvider , который позволяет нам передавать темы через наше приложение. Мы можем изменить наш файл App.js, чтобы добавить эту функциональность.

Давайте посмотрим на это.

 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;

Передавая темы через ThemeProvider , мы можем легко использовать свойства темы в наших стилях.

Например, мы можем установить цвет нашего пользовательского цвета bodyText следующим образом.

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

Мы можем использовать пользовательские темы везде, где нам нужен цвет в нашем приложении.

Например, чтобы определить border-bottom , мы делаем следующее.

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

Заключение

Мы начали с изучения Sanity.io, его настройки и подключения к нашему приложению React. Затем мы настроили Redux и использовали язык GROQ для запроса нашего API. Мы увидели, как подключить и использовать Redux в нашем приложении React с помощью react-redux , использовать стилизованные компоненты и темы.

Однако мы только коснулись того, что возможно с этими технологиями. Я рекомендую вам ознакомиться с примерами кода в моем репозитории GitHub и попробовать свои силы в совершенно другом проекте с использованием этих технологий, чтобы изучить и освоить их.

Ресурсы

  • Документация по здравомыслию
  • Как создать блог с помощью Sanity.io от Kapehe
  • Редукс-документация
  • Документация по стилизованным компонентам
  • Шпаргалка по GROQ
  • Документация по пользовательскому интерфейсу материала
  • Промежуточное ПО Redux и побочные эффекты
  • Документация Redux Thunk