Construindo um aplicativo Web com React, Redux e Sanity.io

Publicados: 2022-03-10
Resumo rápido ↬ Headless CMS é uma maneira poderosa e fácil de gerenciar conteúdo e acessar API. Construído no React, o Sanity.io é uma ferramenta perfeita para gerenciamento de conteúdo flexível. Ele pode ser usado para criar aplicativos simples a complexos desde o início. Neste artigo, Ifeanyi explica como criar um aplicativo de listagem simples com Sanity.io e React. Os estados globais serão gerenciados com Redux e o aplicativo será estilizado com styled-components.

A rápida evolução das plataformas digitais impôs sérias limitações aos CMS tradicionais como o Wordpress. Essas plataformas são acopladas, inflexíveis e focadas no projeto, e não no produto. Felizmente, vários CMS headless foram desenvolvidos para enfrentar esses desafios e muitos outros.

Ao contrário do CMS tradicional, o CMS headless, que pode ser descrito como Software as a Service (SaaS), pode ser usado para desenvolver sites, aplicativos móveis, displays digitais e muito mais. Eles podem ser usados ​​em plataformas ilimitadas. Se você está procurando por um CMS independente de plataforma, que prioriza o desenvolvedor e oferece suporte a várias plataformas, não precisa procurar mais longe do CMS headless.

Um CMS sem cabeça é simplesmente um CMS sem cabeça. A head aqui se refere ao front-end ou à camada de apresentação, enquanto o body se refere ao back-end ou ao repositório de conteúdo. Isso oferece muitos benefícios interessantes. Por exemplo, ele permite que o desenvolvedor escolha qualquer frontend de sua escolha e você também pode projetar a camada de apresentação como desejar.

Existem muitos CMS headless por aí, alguns dos mais populares incluem Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus, etc. Esses CMS headless são baseados em API e têm seus pontos fortes individuais. Por exemplo, CMS como Sanity, Strapi, Contentful e Storyblok são gratuitos para pequenos projetos.

Esses CMS headless também são baseados em diferentes pilhas de tecnologia. Enquanto Sanity.io é baseado em React.js, Storyblok é baseado em Vue.js. Como desenvolvedor do React, essa é a principal razão pela qual rapidamente me interessei pela Sanity. No entanto, sendo um CMS headless, cada uma dessas plataformas pode ser conectada em qualquer frontend, seja Angular, Vue ou React.

Cada um desses CMS headless possui planos gratuitos e pagos que representam um aumento significativo de preços. Embora esses planos pagos ofereçam mais recursos, você não gostaria de pagar tanto por um projeto de pequeno a médio porte. A Sanity tenta resolver esse problema introduzindo opções de pagamento conforme o uso. Com essas opções, você poderá pagar pelo que usar e evitar o salto de preço.

Outra razão pela qual escolho o Sanity.io é a linguagem GROQ. Para mim, a Sanity se destaca da multidão ao oferecer essa ferramenta. O Graphical-Relational Object Queries (GROQ) reduz o tempo de desenvolvimento, ajuda você a obter o conteúdo que você precisa no formato que você precisa e também ajuda o desenvolvedor a criar um documento com um novo modelo de conteúdo sem alterações de código.

Além disso, os desenvolvedores não estão restritos à linguagem GROQ. Você também pode usar o GraphQL ou até mesmo os axios tradicionais e fetch em seu aplicativo React para consultar o backend. Como a maioria dos outros CMS headless, o Sanity possui uma documentação abrangente que contém dicas úteis para construir na plataforma.

Nota: Este artigo requer uma compreensão básica de React, Redux e CSS.

Mais depois do salto! Continue lendo abaixo ↓

Introdução ao Sanity.io

Para usar o Sanity em sua máquina, você precisará instalar a ferramenta Sanity CLI. Embora isso possa ser instalado localmente em seu projeto, é preferível instalá-lo globalmente para torná-lo acessível a qualquer aplicativo futuro.

Para fazer isso, digite os seguintes comandos no seu terminal.

 npm install -g @sanity/cli

O sinalizador -g no comando acima habilita a instalação global.

Em seguida, precisamos inicializar o Sanity em nosso aplicativo. Embora isso possa ser instalado como um projeto separado, geralmente é preferível instalá-lo em seu aplicativo frontend (neste caso, React).

Em seu blog, Kapehe explicou em detalhes como integrar Sanity com React. Será útil ler o artigo antes de continuar com este tutorial.

Digite os seguintes comandos para inicializar o Sanity em seu aplicativo React.

 sanity init

O comando sanity fica disponível para nós quando instalamos a ferramenta Sanity CLI. Você pode visualizar uma lista dos comandos Sanity disponíveis digitando sanity ou sanity help em seu terminal.

Ao configurar ou inicializar seu projeto, você precisará seguir as instruções para personalizá-lo. Você também precisará criar um conjunto de dados e poderá até escolher seu conjunto de dados personalizado preenchido com dados. Para este aplicativo de listagem, usaremos o conjunto de dados de filmes de ficção científica personalizados da Sanity. Isso nos salvará de inserir os dados por conta própria.

Para visualizar e editar seu conjunto de dados, cd para o subdiretório Sanity em seu terminal e digite sanity start . Isso geralmente é executado em https://localhost:3333/ . Pode ser necessário fazer login para acessar a interface (certifique-se de fazer login com a mesma conta que usou ao inicializar o projeto). Uma captura de tela do ambiente é mostrada abaixo.

Visão geral do servidor Sanity
Uma visão geral do servidor de sanidade para o conjunto de dados de filmes de ficção científica. (Visualização grande)

Comunicação bidirecional Sanity-React

Sanity e React precisam se comunicar entre si para uma aplicação totalmente funcional.

Configuração de origens CORS no gerenciador de sanidade

Primeiro, conectaremos nosso aplicativo React ao Sanity. Para fazer isso, faça login em https://manage.sanity.io/ e localize CORS origins em API Settings na guia Settings . Aqui, você precisará conectar sua origem de front-end ao back-end do Sanity. Nosso aplicativo React é executado em https://localhost:3000/ por padrão, então precisamos adicioná-lo ao CORS.

Isso é mostrado na figura abaixo.

Configurações de origem CORS
Configurando a origem CORS no Sanity.io Manager. (Visualização grande)

Conectando Sanidade Para Reagir

A Sanity associa um project ID a cada projeto que você cria. Esse ID é necessário ao conectá-lo ao seu aplicativo front-end. Você pode encontrar o ID do projeto em seu Sanity Manager.

O backend se comunica com o React usando uma biblioteca conhecida como sanity client . Você precisa instalar esta biblioteca em seu projeto Sanity digitando os seguintes comandos.

 npm install @sanity/client

Crie um arquivo sanitySetup.js (o nome do arquivo não importa), na pasta src do seu projeto e insira os seguintes códigos React para configurar uma conexão entre Sanity e React.

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

Passamos nosso projectId , dataset name do conjunto de dados e um booleano useCdn para a instância do cliente de sanidade importado de @sanity/client . Isso faz a mágica e conecta nosso aplicativo ao back-end.

Agora que completamos a conexão bidirecional, vamos direto para a construção do nosso projeto.

Configurando e conectando o Redux ao nosso aplicativo

Precisaremos de algumas dependências para trabalhar com Redux em nosso aplicativo React. Abra seu terminal em seu ambiente React e digite os seguintes comandos bash.

 npm install redux react-redux redux-thunk

Redux é uma biblioteca de gerenciamento de estado global que pode ser usada com a maioria dos frameworks e bibliotecas de front-end, como React. No entanto, precisamos de uma ferramenta intermediária react-redux para permitir a comunicação entre nossa loja Redux e nosso aplicativo React. O thunk do Redux nos ajudará a retornar uma função em vez de um objeto de ação do Redux.

Embora possamos escrever todo o fluxo de trabalho do Redux em um arquivo, geralmente é mais organizado e melhor separar nossas preocupações. Para isso, dividiremos nosso fluxo de trabalho em três arquivos, a saber, actions , reducers e depois o store . No entanto, também precisamos de um arquivo separado para armazenar os action types , também conhecidos como constants .

Configurando a loja

A loja é o arquivo mais importante no Redux. Ele organiza e empacota os estados e os envia para o nosso aplicativo React.

Aqui está a configuração inicial de nossa loja Redux necessária para conectar nosso fluxo de trabalho Redux.

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

A função createStore neste arquivo recebe três parâmetros: o reducer (obrigatório), o estado inicial e o aprimorador (geralmente um middleware, neste caso, thunk fornecido por applyMiddleware ). Nossos redutores serão armazenados em uma pasta de reducers e vamos combiná-los e exportá-los em um arquivo index.js na pasta de reducers . Este é o arquivo que importamos no código acima. Revisitaremos este arquivo mais tarde.

Introdução à linguagem GROQ da Sanity

A Sanity leva a consulta de dados JSON um passo adiante ao introduzir o GROQ. GROQ significa Graph-Relational Object Queries. De acordo com Sanity.io, GROQ é uma linguagem de consulta declarativa projetada para consultar coleções de documentos JSON sem esquema.

A Sanity ainda fornece o GROQ Playground para ajudar os desenvolvedores a se familiarizarem com a linguagem. No entanto, para acessar o playground, é necessário instalar a visão de sanidade . Execute sanity install @sanity/vision no seu terminal para instalá-lo.

O GROQ tem uma sintaxe semelhante ao GraphQL, mas é mais condensada e mais fácil de ler. Além disso, ao contrário do GraphQL, o GROQ pode ser usado para consultar dados JSON.

Por exemplo, para recuperar todos os itens em nosso documento de filme, usaremos a seguinte sintaxe GROQ.

 *[_type == "movie"]

No entanto, se desejarmos recuperar apenas os _ids e crewMembers em nosso documento de filme. Precisamos especificar esses campos da seguinte forma.

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

Aqui, usamos * para dizer ao GROQ que queremos todos os documentos de _type movie. _type é um atributo na coleção de filmes. Também podemos retornar o tipo como fizemos com _id e crewMembers da seguinte forma:

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

Trabalharemos mais no GROQ implementando-o em nossas ações Redux, mas você pode verificar a documentação do Sanity.io do GROQ para saber mais sobre ele. A folha de consulta GROQ fornece muitos exemplos para ajudá-lo a dominar a linguagem de consulta.

Configurando Constantes

Precisamos de constantes para rastrear os tipos de ação em cada estágio do fluxo de trabalho do Redux. Constantes ajudam a determinar o tipo de ação despachada em cada ponto no tempo. Por exemplo, podemos rastrear quando a API está carregando, totalmente carregada e quando ocorre um erro.

Não precisamos necessariamente definir constantes em um arquivo separado, mas para simplicidade e clareza, essa é geralmente a melhor prática no Redux.

Por convenção, as constantes em Javascript são definidas com letras maiúsculas. Seguiremos as melhores práticas aqui para definir nossas constantes. Aqui está um exemplo de uma constante para denotar solicitações para mover a busca de filmes.

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";

Aqui, criamos uma constante MOVIE_FETCH_REQUEST que denota um tipo de ação de MOVIE_FETCH_REQUEST . Isso nos ajuda a chamar facilmente esse tipo de ação sem usar strings e evitar bugs. Também exportamos a constante para estar disponível em qualquer lugar do nosso projeto.

Da mesma forma, podemos criar outras constantes para buscar tipos de ação que indicam quando a solicitação é bem-sucedida ou falha. Um código completo para o movieConstants.js é fornecido no código abaixo.

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

Aqui definimos várias constantes para buscar um filme ou lista de filmes, ordenar e buscar os filmes mais populares. Observe que definimos constantes para determinar quando a solicitação está sendo loading , bem- successful e com failed .

Da mesma forma, nosso arquivo personConstants.js é fornecido abaixo:

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

Assim como o movieConstants.js , definimos uma lista de constantes para buscar uma pessoa ou pessoas. Também definimos uma constante para contar pessoas. As constantes seguem a convenção descrita para movieConstants.js e também as exportamos para serem acessíveis a outras partes do nosso aplicativo.

Por fim, implementaremos o modo claro e escuro no aplicativo e, assim, teremos outro arquivo de constantes globalConstants.js . Vamos dar uma olhada nisso.

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

Aqui definimos constantes para determinar quando o modo claro ou escuro é despachado. SET_LIGHT_THEME determina quando o usuário alterna para o tema claro e SET_DARK_THEME determina quando o tema escuro é selecionado. Também exportamos nossas constantes conforme mostrado.

Configurando as ações

Por convenção, nossas ações são armazenadas em uma pasta separada. As ações são agrupadas de acordo com seus tipos. Por exemplo, nossas ações de filme são armazenadas em movieActions.js enquanto nossas ações de pessoa são armazenadas no arquivo personActions.js .

Também temos globalActions.js para cuidar de alternar o tema do modo claro para o escuro.

Vamos buscar todos os filmes em 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 }); } };

Lembra quando criamos o arquivo sanitySetup.js para conectar o React ao nosso backend Sanity? Aqui, importamos a configuração para nos permitir consultar nosso back-end de sanidade usando o GROQ. Também importamos algumas constantes exportadas do arquivo movieConstants.js na pasta de constants .

Em seguida, criamos a função de ação fetchAllMovies para buscar todos os filmes em nossa coleção. A maioria dos aplicativos React tradicionais usam axios ou fetch para buscar dados do backend. Mas, embora possamos usar qualquer um deles aqui, estamos usando o GROQ da Sanity. Para entrar no modo GROQ , precisamos chamar a função sanityAPI.fetch() conforme mostrado no código acima. Aqui, sanityAPI é a conexão React-Sanity que configuramos anteriormente. Isso retorna um Promise e, portanto, deve ser chamado de forma assíncrona. Usamos a sintaxe async-await aqui, mas também podemos usar a sintaxe .then .

Como estamos usando o thunk em nosso aplicativo, podemos retornar uma função em vez de um objeto de ação. No entanto, optamos por passar a instrução return em uma linha.

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

Observe que também podemos escrever a função desta forma:

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

Em geral, para buscar todos os filmes, primeiro despachamos um tipo de ação que rastreia quando a solicitação ainda está sendo carregada. Em seguida, usamos a sintaxe GROQ da Sanity para consultar de forma assíncrona o documento do filme. Recuperamos o _id e o URL do pôster dos dados do filme. Em seguida, retornamos um payload contendo os dados obtidos da API.

Da mesma forma, podemos recuperar filmes por seu _id , classificar filmes e obter os filmes mais populares.

Também podemos buscar filmes que correspondam à referência de uma determinada pessoa. Fizemos isso na função 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 }); } };

Esta função recebe um argumento e verifica se person._ref em castMembers ou crewMembers corresponde ao argumento passado. Retornamos o _id do filme , o poster url e title ao lado. Também despachamos uma ação do tipo MOVIES_REF_FETCH_SUCCESS , anexando uma carga útil dos dados retornados, e se ocorrer um erro, despachamos uma ação do tipo MOVIE_REF_FETCH_FAIL , anexando uma carga útil da mensagem de erro, graças ao wrapper try-catch .

Na função fetchMovieById , usamos GROQ para recuperar um filme que corresponde a um id específico passado para a função.

A sintaxe GROQ para a função é mostrada abaixo.

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

Assim como a ação fetchAllMovies , começamos selecionando todos os documentos do tipo movie , mas fomos além selecionando apenas aqueles com um id fornecido para a função. Como pretendemos exibir muitos detalhes para o filme, especificamos vários atributos para recuperar.

Recuperamos o id do filme e também alguns atributos no array castMembers , como ref , characterName , o nome da pessoa e a imagem da pessoa. Também alteramos o alias de castMembers para cast .

Assim como o castMembers , selecionamos alguns atributos do array crewMembers , a saber, ref , department , job , o nome da pessoa e a imagem da pessoa. também mudamos o alias de crewMembers para crew .

Da mesma forma, selecionamos o texto geral, popularidade, URL do pôster do filme, data de lançamento e título do filme.

A linguagem GROQ da Sanity também nos permite classificar um documento. Para ordenar um item, passamos ordem ao lado de um operador de tubulação .

Por exemplo, se quisermos classificar os filmes por data de releaseDate em ordem crescente, podemos fazer o seguinte.

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

Usamos essa noção na função sortMoviesBy para classificar por ordem crescente ou decrescente.

Vamos dar uma olhada nesta função abaixo.

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

Começamos enviando uma ação do tipo MOVIES_SORT_REQUEST para determinar quando a solicitação está sendo carregada. Em seguida, usamos a sintaxe GROQ para classificar e buscar dados da coleção de movie . O item para classificar é fornecido na variável item e o modo de classificação (crescente ou decrescente) é fornecido na variável type . Consequentemente, retornamos o id , o URL do pôster e o título. Uma vez que os dados são retornados, despachamos uma ação do tipo MOVIES_SORT_SUCCESS e se falhar, despachamos uma ação do tipo MOVIES_SORT_FAIL .

Um conceito GROQ semelhante se aplica à função getMostPopular . A sintaxe GROQ é mostrada abaixo.

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

A única diferença aqui é que classificamos os filmes por popularidade em ordem decrescente e selecionamos apenas os três primeiros. Os itens são retornados em um índice baseado em zero e, portanto, os três primeiros itens são os itens 0, 1 e 2. Se desejarmos recuperar os dez primeiros itens, podemos passar [0..9] para a função.

Aqui está o código completo para as ações do filme no arquivo 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 };

Configurando os redutores

Os redutores são um dos conceitos mais importantes do Redux. Eles pegam o estado anterior e determinam as mudanças de estado.

Normalmente, usaremos a instrução switch para executar uma condição para cada tipo de ação. Por exemplo, podemos retornar o loading quando o tipo de ação denota carregamento e, em seguida, a carga útil quando denota sucesso ou erro. Espera-se que leve no initial state e a action como argumentos.

Nosso arquivo movieReducers.js contém vários redutores para corresponder às ações definidas no arquivo movieActions.js . No entanto, cada um dos redutores tem uma sintaxe e estrutura semelhantes. As únicas diferenças são as constants que eles chamam e os valores que eles retornam.

Vamos começar dando uma olhada no fetchAllMoviesReducer no arquivo 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; } };

Como todos os redutores, o fetchAllMoviesReducer recebe o objeto de estado inicial ( state ) e o objeto de action como argumentos. Usamos a instrução switch para verificar os tipos de ação em cada momento. Se corresponder a MOVIES_FETCH_REQUEST , retornamos loading como true para nos permitir mostrar um indicador de carregamento para o usuário.

Se corresponder a MOVIES_FETCH_SUCCESS , desligamos o indicador de carregamento e retornamos a carga útil da ação em uma variável movies . Mas se for MOVIES_FETCH_FAIL , também desativamos o carregamento e retornamos o erro. Também queremos a opção de redefinir nossos filmes. Isso nos permitirá limpar os estados quando for necessário.

Temos a mesma estrutura para outros redutores. O movieReducers.js completo é mostrado abaixo.

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

Também seguimos exatamente a mesma estrutura para personReducers.js . Por exemplo, a função fetchAllPersonsReducer define os estados para buscar todas as pessoas no banco de dados.

Isso é dado no código abaixo.

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

Assim como o fetchAllMoviesReducer , definimos fetchAllPersonsReducer com state e action como argumentos. Estas são configurações padrão para redutores Redux. Em seguida, usamos a instrução switch para verificar os tipos de ação e, se for do tipo PERSONS_FETCH_REQUEST , retornamos o carregamento como true. Se for PERSONS_FETCH_SUCCESS , desativamos o carregamento e retornamos o payload, e se for PERSONS_FETCH_FAIL , retornamos o erro.

Combinando Redutores

A função combineReducers do Redux nos permite combinar mais de um redutor e passá-lo para a loja. Combinaremos nossos redutores de filmes e pessoas em um arquivo index.js dentro da pasta de reducers .

Vamos dar uma olhada nisso.

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

Aqui, importamos todos os redutores do arquivo de filmes, pessoas e redutores globais e os passamos para a função combineReducers . A função combineReducers recebe um objeto que nos permite passar todos os nossos redutores. Podemos até adicionar um alias aos argumentos no processo.

Trabalharemos nos globalReducers mais tarde.

Agora podemos passar os redutores no arquivo Redux store.js . Isso é mostrado abaixo.

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

Tendo configurado nosso fluxo de trabalho Redux, vamos configurar nosso aplicativo React.

Configurando nosso aplicativo React

Nosso aplicativo de reação listará os filmes e seu elenco e membros da equipe correspondentes. Usaremos react-router-dom para roteamento e styled-components para estilizar o aplicativo. Também usaremos Material UI para ícones e alguns componentes de UI.

Digite o seguinte comando bash para instalar as dependências.

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

Aqui está o que vamos construir:

Conectando o Redux ao nosso aplicativo React

React-redux vem com uma função Provider que nos permite conectar nosso aplicativo à loja Redux. Para fazer isso, temos que passar uma instância da loja para o Provedor. Podemos fazer isso em nosso arquivo index.js ou App.js

Aqui está nosso arquivo 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") );

Aqui, importamos o Provider do react-redux e store da nossa loja Redux. Em seguida, agrupamos toda a nossa árvore de componentes com o Provider, passando a loja para ele.

Em seguida, precisamos react-router-dom para roteamento em nosso aplicativo React. react react-router-dom vem com BrowserRouter , Switch e Route que podem ser usados ​​para definir nosso caminho e rotas.

Fazemos isso em nosso arquivo App.js Isso é mostrado abaixo.

 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;

Esta é uma configuração padrão para roteamento com react-router-dom. Você pode conferir na documentação deles. Importamos nossos componentes Header , Footer , PersonsList e MovieList . Em seguida, configuramos o react-router-dom envolvendo tudo em Router e Switch .

Como queremos que nossas páginas compartilhem o mesmo cabeçalho e rodapé, tivemos que passar o componente <Header /> e <Footer /> antes de envolver a estrutura com Switch . Também fizemos algo semelhante com o elemento main , pois queremos que ele envolva todo o aplicativo.

Passamos cada componente para a rota usando Route de react-router-dom .

Definindo nossas páginas e componentes

Nosso aplicativo está organizado de forma estruturada. Os componentes reutilizáveis ​​são armazenados na pasta de components , enquanto as páginas são armazenadas na pasta de pages .

Nossas pages incluem movieListPage.js , moviePage.js , PersonListPage.js e PersonPage.js . O MovieListPage.js lista todos os filmes em nosso back-end Sanity.io, bem como os filmes mais populares.

Para listar todos os filmes, simplesmente dispatch a ação fetchAllMovies definida em nosso arquivo movieAction.js . Como precisamos buscar a lista assim que a página carregar, temos que defini-la no useEffect . Isso é mostrado abaixo.

 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;

Graças aos Hooks useDispatch e useSelector , podemos despachar ações do Redux e selecionar os estados apropriados do armazenamento do Redux. Observe que os estados loading , error e movies foram definidos em nossas funções Reducer e aqui os selecionamos usando o Hook useSelector do React Redux. Esses estados, ou seja, loading , error e movies ficam disponíveis imediatamente quando despachamos as ações fetchAllMovies() .

Assim que obtivermos a lista de filmes, podemos exibi-la em nosso aplicativo usando a função map ou como desejarmos.

Aqui está o código completo para o arquivo 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

Começamos despachando a ação de filmes getMostPopular (esta ação seleciona os filmes com maior popularidade) no gancho useEffect . Isso nos permite recuperar os filmes mais populares assim que a página for carregada. Além disso, permitimos que os usuários classificassem os filmes por data de releaseDate e popularity . Isso é tratado pela ação sortMoviesBy despachada no código acima. Além disso, despachamos o fetchAllMovies dependendo dos parâmetros de consulta.

Além disso, usamos o gancho useSelector para selecionar os redutores correspondentes para cada uma dessas ações. Selecionamos os estados para loading , error e movies para cada um dos redutores.

Depois de obter os movies dos redutores, agora podemos exibi-los ao usuário. Aqui, usamos a função de map ES6 para fazer isso. Primeiro exibimos um carregador sempre que cada um dos estados do filme está sendo carregado e, se houver um erro, exibimos a mensagem de erro. Finalmente, se obtivermos um filme, exibimos a imagem do filme para o usuário usando a função map . Envolvemos todo o componente em um componente MovieListContainer .

A <MovieListContainer> … </MovieListContainer> é uma div definida usando componentes estilizados. Daremos uma breve olhada nisso em breve.

Estilizando nosso aplicativo com componentes estilizados

Componentes estilizados nos permitem estilizar nossas páginas e componentes individualmente. Ele também oferece alguns recursos interessantes, como inheritance , Theming , passing of props , etc.

Embora sempre desejemos estilizar nossas páginas individualmente, às vezes o estilo global pode ser desejável. Curiosamente, os styled-components fornecem uma maneira de fazer isso, graças à função createGlobalStyle .

Para usar componentes com estilo em nosso aplicativo, precisamos instalá-lo. Abra seu terminal em seu projeto react e digite o seguinte comando bash .

 npm install styled-components

Tendo instalado os styled-components, vamos começar com nossos estilos globais.

Vamos criar uma pasta separada em nosso diretório src chamada styles . Isso armazenará todos os nossos estilos. Vamos também criar um arquivo globalStyles.js dentro da pasta de estilos. Para criar um estilo global em componentes com estilo, precisamos importar createGlobalStyle .

 import { createGlobalStyle } from "styled-components";

Podemos então definir nossos estilos da seguinte forma:

 export const GlobalStyle = createGlobalStyle` ... `

Componentes estilizados usam o literal de modelo para definir props. Dentro desse literal, podemos escrever nossos códigos CSS tradicionais.

Também importamos deviceWidth definido em um arquivo chamado definition.js . O deviceWidth contém a definição de pontos de interrupção para definir nossas consultas de mídia.

 import { deviceWidth } from "./definition";

Definimos overflow como oculto para controlar o fluxo de nosso aplicativo.

 html, body{ overflow-x: hidden; }

Também definimos o estilo do cabeçalho usando o seletor de estilo .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%; } ... }

Aqui, vários estilos como a cor de fundo, z-index, padding e muitas outras propriedades CSS tradicionais são definidos.

Usamos as props styled-components para definir a cor do plano de fundo. Isso nos permite definir variáveis ​​dinâmicas que podem ser passadas do nosso componente. Além disso, também passamos a variável do tema para que possamos aproveitar ao máximo nossa alternância de temas.

O tema é possível aqui porque envolvemos todo o nosso aplicativo com o ThemeProvider de styled-components. Falaremos sobre isso em um momento. Além disso, usamos o CSS flexbox para estilizar corretamente nosso cabeçalho e definimos a posição como fixed para garantir que ele permaneça fixo em relação ao navegador. Também definimos os pontos de interrupção para tornar os cabeçalhos compatíveis com dispositivos móveis.

Aqui está o código completo para nosso arquivo 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}; } `;

Observe que escrevemos código CSS puro dentro do literal, mas há algumas exceções. Styled-components nos permite passar adereços. Você pode aprender mais sobre isso na documentação.

Além de definir estilos globais, podemos definir estilos para páginas individuais.

Por exemplo, aqui está o estilo para PersonListPage.js definido em PersonStyle.js na pasta de 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; } } } } `;

Primeiro importamos styled de styled-components e deviceWidth do arquivo de definition . Em seguida, definimos PersonsListContainer como um div para armazenar nossos estilos. Usando consultas de mídia e os pontos de interrupção estabelecidos, tornamos a página compatível com dispositivos móveis definindo vários pontos de interrupção.

Aqui, usamos apenas os pontos de interrupção do navegador padrão para telas pequenas, grandes e muito grandes. Também aproveitamos ao máximo o flexbox e a grade CSS para estilizar e exibir adequadamente nosso conteúdo na página.

Para usar esse estilo em nosso arquivo PersonListPage.js , simplesmente o importamos e adicionamos à nossa página da seguinte forma.

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

O wrapper produzirá uma div porque a definimos como uma div em nossos estilos.

Adicionando temas e finalizando

É sempre um recurso legal adicionar temas ao nosso aplicativo. Para isso, precisamos do seguinte:

  • Nossos temas personalizados definidos em um arquivo separado (no nosso caso, arquivo definition.js ).
  • A lógica definida em nossas ações e redutores do Redux.
  • Chamando nosso tema em nosso aplicativo e passando-o pela árvore de componentes.

Vamos verificar isso.

Aqui está nosso objeto de theme no arquivo 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)" } };

Adicionamos várias propriedades de cor para os temas claros e escuros. As cores são cuidadosamente escolhidas para permitir a visibilidade tanto no modo claro quanto no escuro. Você pode definir seus temas como quiser. Esta não é uma regra dura e rápida.

Em seguida, vamos adicionar a funcionalidade ao Redux.

Criamos globalActions.js em nossa pasta de ações do Redux e adicionamos os seguintes códigos.

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

Aqui, simplesmente importamos nossos temas definidos. Despachou as ações correspondentes, passando a carga útil dos temas que precisávamos. Os resultados da carga útil são armazenados no armazenamento local usando as mesmas chaves para temas claros e escuros. Isso nos permite persistir os estados no navegador.

Também precisamos definir nosso redutor para os temas.

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

Isso é muito parecido com o que temos feito. Usamos a instrução switch para verificar o tipo de ação e, em seguida, retornamos a payload apropriada. Também retornamos um estado light que determina se o tema claro ou escuro é selecionado pelo usuário. Usaremos isso em nossos componentes.

Também precisamos adicioná-lo ao nosso redutor raiz e armazenar. Aqui está o código completo para nosso 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));

Como precisávamos persistir o tema quando o usuário atualizasse, tivemos que obtê-lo do armazenamento local usando localStorage.getItem() e passá-lo para nosso estado inicial.

Adicionando a funcionalidade ao nosso aplicativo React

Componentes com estilo nos fornecem ThemeProvider que nos permite passar temas através de nosso aplicativo. Podemos modificar nosso arquivo App.js para adicionar essa funcionalidade.

Vamos dar uma olhada nisso.

 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;

Ao passar os temas pelo ThemeProvider , podemos usar facilmente os adereços do tema em nossos estilos.

Por exemplo, podemos definir a cor para nossa cor personalizada bodyText da seguinte maneira.

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

Podemos usar os temas personalizados em qualquer lugar que precisemos de cores em nosso aplicativo.

Por exemplo, para definir border-bottom , fazemos o seguinte.

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

Conclusão

Começamos investigando o Sanity.io, configurando-o e conectando-o ao nosso aplicativo React. Em seguida, configuramos o Redux e usamos a linguagem GROQ para consultar nossa API. Vimos como conectar e usar o Redux ao nosso aplicativo React usando react-redux , usar styled-components e temas.

No entanto, apenas arranhamos a superfície do que é possível com essas tecnologias. Recomendo que você veja os exemplos de código em meu repositório do GitHub e experimente um projeto completamente diferente usando essas tecnologias para aprender e dominá-las.

Recursos

  • Documentação de Sanidade
  • Como construir um blog com Sanity.io por Kapehe
  • Documentação do Redux
  • Documentação de componentes estilizados
  • Folha de dicas GROQ
  • Documentação da interface do usuário do material
  • Middleware Redux e SideEffects
  • Documentação do Redux Thunk