Construindo um aplicativo Web com React, Redux e Sanity.io
Publicados: 2022-03-10A 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.
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.
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.
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