Creación de una aplicación web con React, Redux y Sanity.io
Publicado: 2022-03-10La rápida evolución de las plataformas digitales ha puesto serias limitaciones a los CMS tradicionales como Wordpress. Estas plataformas son acopladas, inflexibles y enfocadas al proyecto, más que al producto. Afortunadamente, se han desarrollado varios CMS sin cabeza para abordar estos desafíos y muchos más.
A diferencia del CMS tradicional, el CMS sin encabezado, que se puede describir como software como servicio (SaaS), se puede usar para desarrollar sitios web, aplicaciones móviles, pantallas digitales y mucho más. Se pueden utilizar en plataformas ilimitadas. Si está buscando un CMS que sea independiente de la plataforma, que priorice al desarrollador y que ofrezca soporte multiplataforma, no necesita buscar más allá del CMS sin cabeza.
Un CMS sin cabeza es simplemente un CMS sin cabeza. El head
aquí se refiere al frontend o la capa de presentación, mientras que el body
se refiere al backend o al repositorio de contenido. Esto ofrece muchos beneficios interesantes. Por ejemplo, le permite al desarrollador elegir cualquier interfaz de su elección y también puede diseñar la capa de presentación como desee.
Hay muchos CMS sin cabeza, algunos de los más populares incluyen Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus, etc. Estos CMS sin cabeza están basados en API y tienen sus puntos fuertes individuales. Por ejemplo, CMS como Sanity, Strapi, Contentful y Storyblok son gratuitos para proyectos pequeños.
Estos CMS sin cabeza también se basan en diferentes pilas de tecnología. Mientras que Sanity.io se basa en React.js, Storyblok se basa en Vue.js. Como desarrollador de React, esta es la razón principal por la que rápidamente me interesé en Sanity. Sin embargo, al ser un CMS headless, cada una de estas plataformas se puede conectar a cualquier frontend, ya sea Angular, Vue o React.
Cada uno de estos CMS sin cabeza tiene planes gratuitos y pagos que representan un salto de precio significativo. Aunque estos planes pagos ofrecen más funciones, no querrá pagar tanto por un proyecto pequeño o mediano. Sanity intenta resolver este problema introduciendo opciones de pago por uso. Con estas opciones, podrá pagar por lo que usa y evitar el salto de precio.
Otra razón por la que elijo Sanity.io es su lenguaje GROQ. Para mí, Sanity se destaca entre la multitud al ofrecer esta herramienta. Consultas de objetos relacionales gráficos (GROQ) reduce el tiempo de desarrollo, lo ayuda a obtener el contenido que necesita en la forma en que lo necesita y también ayuda al desarrollador a crear un documento con un nuevo modelo de contenido sin cambios de código.
Además, los desarrolladores no están limitados al lenguaje GROQ. También puede usar GraphQL o incluso el tradicional axios
y fetch
en su aplicación React para consultar el backend. Como la mayoría de los otros CMS sin cabeza, Sanity tiene una documentación completa que contiene consejos útiles para construir en la plataforma.
Nota: este artículo requiere una comprensión básica de React, Redux y CSS.
Primeros pasos con Sanity.io
Para usar Sanity en su máquina, deberá instalar la herramienta Sanity CLI. Si bien esto se puede instalar localmente en su proyecto, es preferible instalarlo globalmente para que sea accesible para cualquier aplicación futura.
Para hacer esto, ingrese los siguientes comandos en su terminal.
npm install -g @sanity/cli
El indicador -g
en el comando anterior habilita la instalación global.
A continuación, debemos inicializar Sanity en nuestra aplicación. Aunque esto se puede instalar como un proyecto separado, generalmente es preferible instalarlo dentro de su aplicación frontend (en este caso, React).
En su blog, Kapehe explicó en detalle cómo integrar Sanity con React. Será útil leer el artículo antes de continuar con este tutorial.
Ingrese los siguientes comandos para inicializar Sanity en su aplicación React.
sanity init
El comando sanity
está disponible para nosotros cuando instalamos la herramienta Sanity CLI. Puede ver una lista de los comandos de cordura disponibles escribiendo sanity
o sanity help
de cordura en su terminal.
Al configurar o inicializar su proyecto, deberá seguir las indicaciones para personalizarlo. También se le pedirá que cree un conjunto de datos e incluso puede elegir su conjunto de datos personalizado poblado con datos. Para esta aplicación de listado, utilizaremos el conjunto de datos de películas de ciencia ficción personalizado de Sanity. Esto nos evitará introducir los datos nosotros mismos.
Para ver y editar su conjunto de datos, cd
al subdirectorio Sanity en su terminal e ingrese sanity start
. Esto generalmente se ejecuta en https://localhost:3333/
. Es posible que deba iniciar sesión para acceder a la interfaz (asegúrese de iniciar sesión con la misma cuenta que utilizó al inicializar el proyecto). A continuación se muestra una captura de pantalla del entorno.
Comunicación bidireccional Sanity-React
Sanity y React necesitan comunicarse entre sí para una aplicación completamente funcional.
Configuración de orígenes de CORS en Sanity Manager
Primero conectaremos nuestra aplicación React a Sanity. Para hacer esto, inicie sesión en https://manage.sanity.io/
y localice los CORS origins
en API Settings
en la pestaña Settings
. Aquí, deberá vincular el origen de su interfaz con el backend de Sanity. Nuestra aplicación React se ejecuta en https://localhost:3000/
de manera predeterminada, por lo que debemos agregarla al CORS.
Esto se muestra en la siguiente figura.
Conectando la cordura para reaccionar
Sanity asocia un project ID
a cada proyecto que crea. Esta identificación es necesaria cuando se conecta a su aplicación frontend. Puede encontrar el ID del proyecto en su Sanity Manager.
El backend se comunica con React usando una biblioteca conocida como sanity client
. Debe instalar esta biblioteca en su proyecto Sanity ingresando los siguientes comandos.
npm install @sanity/client
Cree un archivo sanitySetup.js
(el nombre del archivo no importa), en la carpeta src
de su proyecto e ingrese los siguientes códigos React para configurar una conexión entre Sanity y React.
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
Pasamos nuestro projectId
, el dataset name
del conjunto de datos y un useCdn
booleano a la instancia del cliente de cordura importado de @sanity/client
. Esto hace la magia y conecta nuestra aplicación al backend.
Ahora que hemos completado la conexión bidireccional, pasemos directamente a construir nuestro proyecto.
Configuración y conexión de Redux a nuestra aplicación
Necesitaremos algunas dependencias para trabajar con Redux en nuestra aplicación React. Abra su terminal en su entorno React e ingrese los siguientes comandos bash.
npm install redux react-redux redux-thunk
Redux es una biblioteca de administración de estado global que se puede usar con la mayoría de los marcos y bibliotecas frontend como React. Sin embargo, necesitamos una herramienta intermediaria, react-redux
para habilitar la comunicación entre nuestra tienda Redux y nuestra aplicación React. Redux thunk nos ayudará a devolver una función en lugar de un objeto de acción de Redux.
Si bien podríamos escribir todo el flujo de trabajo de Redux en un solo archivo, a menudo es más ordenado y mejor separar nuestras preocupaciones. Para esto, dividiremos nuestro flujo de trabajo en tres archivos, a saber, actions
, reducers
y luego store
. Sin embargo, también necesitamos un archivo separado para almacenar los action types
, también conocidos como constants
.
Configuración de la tienda
La tienda es el archivo más importante en Redux. Organiza y empaqueta los estados y los envía a nuestra aplicación React.
Aquí está la configuración inicial de nuestra tienda Redux necesaria para conectar nuestro flujo de trabajo Redux.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
La función createStore
en este archivo toma tres parámetros: el reducer
(requerido), el estado inicial y el potenciador (generalmente un middleware, en este caso, thunk
proporcionado a través applyMiddleware
). Nuestros reductores se almacenarán en una carpeta de reducers
y los combinaremos y exportaremos en un archivo index.js
en la carpeta de reducers
. Este es el archivo que importamos en el código anterior. Revisaremos este archivo más tarde.
Introducción al lenguaje GROQ de Sanity
Sanity lleva la consulta de datos JSON un paso más allá al presentar GROQ. GROQ son las siglas de Graph-Relational Object Queries. Según Sanity.io, GROQ es un lenguaje de consulta declarativo diseñado para consultar colecciones de documentos JSON en gran parte sin esquema.
Sanity incluso proporciona GROQ Playground para ayudar a los desarrolladores a familiarizarse con el lenguaje. Sin embargo, para acceder al área de juegos, debe instalar Sanity Vision . Ejecute sanity install @sanity/vision
en su terminal para instalarlo.
GROQ tiene una sintaxis similar a GraphQL pero es más condensada y fácil de leer. Además, a diferencia de GraphQL, GROQ se puede usar para consultar datos JSON.
Por ejemplo, para recuperar todos los elementos de nuestro documento de película, utilizaremos la siguiente sintaxis GROQ.
*[_type == "movie"]
Sin embargo, si deseamos recuperar solo los _ids
y los miembros de la crewMembers
en nuestro documento de película. Necesitamos especificar esos campos de la siguiente manera.
`*[_type == 'movie']{ _id, crewMembers }
Aquí, usamos *
para decirle a GROQ que queremos todos los documentos de _type
movie. _type
es un atributo de la colección de películas. También podemos devolver el tipo como hicimos _id
y crewMembers
de la siguiente manera:
*[_type == 'movie']{ _id, _type, crewMembers }
Trabajaremos más en GROQ al implementarlo en nuestras acciones de Redux, pero puede consultar la documentación de Sanity.io para GROQ para obtener más información al respecto. La hoja de trucos de consulta de GROQ proporciona muchos ejemplos para ayudarlo a dominar el lenguaje de consulta.
Configuración de constantes
Necesitamos constantes para rastrear los tipos de acción en cada etapa del flujo de trabajo de Redux. Las constantes ayudan a determinar el tipo de acción despachada en cada momento. Por ejemplo, podemos realizar un seguimiento cuando la API se está cargando, completamente cargada y cuando se produce un error.
No necesariamente necesitamos definir constantes en un archivo separado, pero por simplicidad y claridad, esta suele ser la mejor práctica en Redux.
Por convención, las constantes en Javascript se definen con mayúsculas. Seguiremos las mejores prácticas aquí para definir nuestras constantes. Este es un ejemplo de una constante para denotar solicitudes de obtención de películas en movimiento.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
Aquí, creamos una constante MOVIE_FETCH_REQUEST
que denota un tipo de acción de MOVIE_FETCH_REQUEST
. Esto nos ayuda a llamar fácilmente a este tipo de acción sin usar strings
y evitar errores. También exportamos la constante para que esté disponible en cualquier parte de nuestro proyecto.
De manera similar, podemos crear otras constantes para obtener tipos de acción que indiquen cuándo la solicitud tiene éxito o falla. En el siguiente código se proporciona un código completo para movieConstants.js
.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";
Aquí hemos definido varias constantes para buscar una película o una lista de películas, ordenar y buscar las películas más populares. Tenga en cuenta que establecemos constantes para determinar cuándo se está loading
la solicitud, si tiene successful
o si failed
.
De manera similar, nuestro archivo personConstants.js
se proporciona a continuación:
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";
Al igual que movieConstants.js
, establecemos una lista de constantes para buscar una persona o personas. También establecemos una constante para contar personas. Las constantes siguen la convención descrita para movieConstants.js
y también las exportamos para que sean accesibles a otras partes de nuestra aplicación.
Finalmente, implementaremos el modo claro y oscuro en la aplicación y así tenemos otro archivo de constantes globalConstants.js
. Echémosle un vistazo.
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
Aquí establecemos constantes para determinar cuándo se envía el modo claro u oscuro. SET_LIGHT_THEME
determina cuándo el usuario cambia al tema claro y SET_DARK_THEME
determina cuándo se selecciona el tema oscuro. También exportamos nuestras constantes como se muestra.
Configuración de las acciones
Por convención, nuestras acciones se almacenan en una carpeta separada. Las acciones se agrupan según sus tipos. Por ejemplo, nuestras acciones de películas se almacenan en movieActions.js
mientras que nuestras acciones de persona se almacenan en el archivo personActions.js
.
También tenemos globalActions.js
para encargarse de alternar el tema del modo claro al oscuro.
Busquemos todas las películas en 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 }); } };
¿Recuerdas cuando creamos el archivo sanitySetup.js
para conectar React a nuestro backend de Sanity? Aquí, importamos la configuración para permitirnos consultar nuestro backend de cordura usando GROQ. También importamos algunas constantes exportadas del archivo movieConstants.js
en la carpeta de constants
.
A continuación, creamos la función de acción fetchAllMovies
para obtener todas las películas de nuestra colección. La mayoría de las aplicaciones React tradicionales usan axios
o fetch
para obtener datos del backend. Pero aunque podríamos usar cualquiera de estos aquí, estamos usando GROQ
de Sanity. Para ingresar al modo GROQ
, debemos llamar a la función sanityAPI.fetch()
como se muestra en el código anterior. Aquí, sanityAPI
es la conexión React-Sanity que configuramos anteriormente. Esto devuelve una Promise
y, por lo tanto, debe llamarse de forma asíncrona. Hemos usado la sintaxis async-await
aquí, pero también podemos usar la sintaxis .then
.
Como estamos usando thunk
en nuestra aplicación, podemos devolver una función en lugar de un objeto de acción. Sin embargo, elegimos pasar la declaración de devolución en una línea.
const fetchAllMovies = () => async (dispatch) => { ... }
Tenga en cuenta que también podemos escribir la función de esta manera:
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
En general, para obtener todas las películas, primero enviamos un tipo de acción que realiza un seguimiento cuando la solicitud aún se está cargando. Luego usamos la sintaxis GROQ de Sanity para consultar de forma asíncrona el documento de la película. Recuperamos el _id
y la URL del póster de los datos de la película. Luego devolvimos una carga útil que contenía los datos obtenidos de la API.
Del mismo modo, podemos recuperar películas por su _id
, ordenar películas y obtener las películas más populares.
También podemos buscar películas que coincidan con la referencia de una persona en particular. Hicimos esto en la función 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 función toma un argumento y verifica si person._ref
en castMembers
o crewMembers
coincide con el argumento pasado. Devolvemos el _id
de la película, la poster url
y el title
al lado. También enviamos una acción de tipo MOVIES_REF_FETCH_SUCCESS
, adjuntando una carga útil de los datos devueltos, y si ocurre un error, enviamos una acción de tipo MOVIE_REF_FETCH_FAIL
, adjuntando una carga útil del mensaje de error, gracias al contenedor try-catch
.
En la función fetchMovieById
, usamos GROQ
para recuperar una película que coincida con una id
particular pasada a la función.
La sintaxis GROQ
para la función se muestra a continuación.
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]` );
Al igual que la acción fetchAllMovies
, comenzamos seleccionando todos los documentos de tipo movie
, pero fuimos más allá y seleccionamos solo aquellos con una identificación proporcionada a la función. Dado que tenemos la intención de mostrar una gran cantidad de detalles de la película, especificamos un montón de atributos para recuperar.
Recuperamos la id
de la película y también algunos atributos en la matriz castMembers
, a saber, ref
, characterName
, el nombre de la persona y la imagen de la persona. También cambiamos el alias de castMembers
a cast
.
Al igual que castMembers
, seleccionamos algunos atributos de la matriz crewMembers
, a saber, ref
, department
, job
, el nombre de la persona y la imagen de la persona. también cambiamos el alias de crewMembers
a crew
.
De la misma manera, seleccionamos el texto general, la popularidad, la URL del póster de la película, la fecha de estreno y el título de la película.
El lenguaje GROQ de Sanity también nos permite ordenar un documento. Para ordenar un artículo, pasamos order al lado de un operador de canalización .
Por ejemplo, si deseamos ordenar las películas por fecha de releaseDate
en orden ascendente, podríamos hacer lo siguiente.
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
Usamos esta noción en la función sortMoviesBy
para clasificar en orden ascendente o descendente.
Echemos un vistazo a esta función a continuación.
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 }); } };
Comenzamos enviando una acción de tipo MOVIES_SORT_REQUEST
para determinar cuándo se está cargando la solicitud. Luego usamos la sintaxis GROQ
para ordenar y obtener datos de la colección de movie
. El elemento por el que ordenar se proporciona en el item
variable y el modo de clasificación (ascendente o descendente) se proporciona en el type
de variable. En consecuencia, devolvimos la id
, la URL del póster y el título. Una vez que se devuelven los datos, enviamos una acción de tipo MOVIES_SORT_SUCCESS
y, si falla, enviamos una acción de tipo MOVIES_SORT_FAIL
.
Un concepto GROQ
similar se aplica a la función getMostPopular
. La sintaxis GROQ
se muestra a continuación.
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]` );
La única diferencia aquí es que clasificamos las películas por popularidad en orden descendente y luego seleccionamos solo las tres primeras. Los elementos se devuelven en un índice basado en cero, por lo que los primeros tres elementos son los elementos 0, 1 y 2. Si deseamos recuperar los primeros diez elementos, podemos pasar [0..9]
a la función.
Aquí está el código completo para las acciones de la película en el archivo 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 };
Configuración de los reductores
Los reductores son uno de los conceptos más importantes en Redux. Toman el estado anterior y determinan los cambios de estado.
Por lo general, usaremos la declaración de cambio para ejecutar una condición para cada tipo de acción. Por ejemplo, podemos devolver loading
cuando el tipo de acción denota cargando, y luego la carga útil cuando denota éxito o error. Se espera que tome como argumentos el initial state
y la action
.
Nuestro archivo movieReducers.js
contiene varios reductores para que coincidan con las acciones definidas en el archivo movieActions.js
. Sin embargo, cada uno de los reductores tiene una sintaxis y una estructura similares. Las únicas diferencias son las constants
que llaman y los valores que devuelven.
Comencemos por echar un vistazo a fetchAllMoviesReducer
en el archivo 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 los reductores, fetchAllMoviesReducer
toma el objeto de estado inicial ( state
) y el objeto de action
como argumentos. Usamos la instrucción switch para verificar los tipos de acción en cada momento. Si corresponde a MOVIES_FETCH_REQUEST
, devolvemos loading como verdadero para permitirnos mostrar un indicador de carga al usuario.
Si corresponde a MOVIES_FETCH_SUCCESS
, apagamos el indicador de carga y luego devolvemos el payload de acción en una variable movies
. Pero si es MOVIES_FETCH_FAIL
, también apagamos la carga y luego devolvemos el error. También queremos la opción de restablecer nuestras películas. Esto nos permitirá borrar los estados cuando sea necesario.

Tenemos la misma estructura para otros reductores. El movieReducers.js
completo se muestra a continuación.
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 };
También seguimos exactamente la misma estructura para personReducers.js
. Por ejemplo, la función fetchAllPersonsReducer
define los estados para obtener todas las personas en la base de datos.
Esto se da en el código a continuación.
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; } };
Al igual que fetchAllMoviesReducer
, definimos fetchAllPersonsReducer
con state
y action
como argumentos. Estas son configuraciones estándar para reductores Redux. Luego usamos la declaración de cambio para verificar los tipos de acción y, si es del tipo PERSONS_FETCH_REQUEST
, devolvemos la carga como verdadera. Si es PERSONS_FETCH_SUCCESS
, desactivamos la carga y devolvemos la carga útil, y si es PERSONS_FETCH_FAIL
, devolvemos el error.
Combinación de reductores
La función combineReducers
de Redux nos permite combinar más de un reductor y pasarlo a la tienda. Combinaremos nuestros reductores de películas y personas en un archivo index.js
dentro de la carpeta reducers
.
Echémosle un vistazo.
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 });
Aquí importamos todos los reductores del archivo de películas, personas y reductores globales y los pasamos a la función combineReducers
. La función combineReducers
toma un objeto que nos permite pasar todos nuestros reductores. Incluso podemos agregar un alias a los argumentos en el proceso.
Trabajaremos en los globalReducers
más tarde.
Ahora podemos pasar los reductores en el archivo store.js
de Redux. Esto se muestra a continuación.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
Habiendo configurado nuestro flujo de trabajo Redux, configuremos nuestra aplicación React.
Configurando nuestra aplicación React
Nuestra aplicación de reacción mostrará una lista de películas y su elenco y miembros del equipo correspondientes. Usaremos react-router-dom
para el enrutamiento y styled-components
con estilo para diseñar la aplicación. También usaremos Material UI para íconos y algunos componentes de UI.
Ingrese el siguiente comando bash
para instalar las dependencias.
npm install react-router-dom @material-ui/core @material-ui/icons query-string
Esto es lo que construiremos:
Conexión de Redux a nuestra aplicación React
React-redux
se envía con una función de proveedor que nos permite conectar nuestra aplicación a la tienda Redux. Para hacer esto, tenemos que pasar una instancia de la tienda al Proveedor. Podemos hacer esto en nuestro archivo index.js
o App.js
Aquí está nuestro archivo 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") );
Aquí, importamos Provider
desde react-redux
y store
desde nuestra tienda Redux. Luego envolvimos todo nuestro árbol de componentes con el Proveedor, pasándole la tienda.
A continuación, necesitamos react-router-dom
para el enrutamiento en nuestra aplicación React. react-router-dom
viene con BrowserRouter
, Switch
y Route
que se pueden usar para definir nuestra ruta y rutas.
Hacemos esto en nuestro archivo App.js
Esto se muestra a continuación.
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 es una configuración estándar para el enrutamiento con react-router-dom. Puedes comprobarlo en su documentación. Importamos nuestros componentes Header
, Footer
, PersonsList
y MovieList
. Luego configuramos el react-router-dom
envolviendo todo en Router
y Switch
.
Como queremos que nuestras páginas compartan el mismo encabezado y pie de página, tuvimos que pasar el componente <Header />
y <Footer />
antes de envolver la estructura con Switch
. También hicimos algo similar con el elemento main
, ya que queremos que envuelva toda la aplicación.
Pasamos cada componente a la ruta usando Route
from react-router-dom
.
Definición de nuestras páginas y componentes
Nuestra aplicación está organizada de forma estructurada. Los componentes reutilizables se almacenan en la carpeta de components
mientras que las páginas se almacenan en la carpeta de pages
.
Nuestras pages
incluyen movieListPage.js
, moviePage.js
, PersonListPage.js
y PersonPage.js
. MovieListPage.js
enumera todas las películas en nuestro backend de Sanity.io, así como las películas más populares.
Para listar todas las películas, simplemente dispatch
la acción fetchAllMovies
definida en nuestro archivo movieAction.js
. Dado que necesitamos obtener la lista tan pronto como se carga la página, debemos definirla en useEffect
. Esto se muestra a continuación.
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;
Gracias a useDispatch
y useSelector
Hooks, podemos enviar acciones de Redux y seleccionar los estados apropiados de la tienda de Redux. Tenga en cuenta que los estados loading
, error
y movies
se definieron en nuestras funciones Reducer y aquí los seleccionó usando useSelector
Hook de React Redux. Estos estados, a saber, la loading
, error
y movies
, están disponibles inmediatamente cuando enviamos las acciones fetchAllMovies()
.
Una vez que obtengamos la lista de películas, podemos mostrarla en nuestra aplicación usando la función de map
o como queramos.
Aquí está el código completo para el archivo 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
Comenzamos enviando la acción de películas getMostPopular
(esta acción selecciona las películas con la mayor popularidad) en useEffect
Hook. Esto nos permite recuperar las películas más populares tan pronto como se carga la página. Además, permitimos a los usuarios clasificar las películas por fecha de releaseDate
y popularity
. Esto es manejado por la acción sortMoviesBy
despachada en el código anterior. Además, enviamos fetchAllMovies
según los parámetros de consulta.
Además, usamos el useSelector
Hook para seleccionar los reductores correspondientes para cada una de estas acciones. Seleccionamos los estados de loading
, error
y movies
para cada uno de los reductores.
Después de obtener las movies
de los reductores, ahora podemos mostrárselas al usuario. Aquí, hemos utilizado la función de map
ES6 para hacer esto. Primero mostramos un cargador cada vez que se está cargando cada uno de los estados de la película y, si hay un error, mostramos el mensaje de error. Finalmente, si obtenemos una película, mostramos la imagen de la película al usuario usando la función de map
. Envolvimos todo el componente en un componente MovieListContainer
.
La <MovieListContainer> … </MovieListContainer>
es un div
definido mediante componentes con estilo. Echaremos un breve vistazo a eso pronto.
Diseñar nuestra aplicación con componentes con estilo
Los componentes con estilo nos permiten diseñar nuestras páginas y componentes de forma individual. También ofrece algunas características interesantes como la inheritance
, Theming
, el passing of props
, etc.
Aunque siempre queremos diseñar nuestras páginas de forma individual, a veces puede ser deseable un estilo global. Curiosamente, los componentes con estilo proporcionan una forma de hacerlo, gracias a la función createGlobalStyle
.
Para usar componentes con estilo en nuestra aplicación, necesitamos instalarlo. Abra su terminal en su proyecto de reacción e ingrese el siguiente comando bash
.
npm install styled-components
Habiendo instalado los componentes con estilo, comencemos con nuestros estilos globales.
Vamos a crear una carpeta separada en nuestro directorio src
llamada styles
. Esto almacenará todos nuestros estilos. También vamos a crear un archivo globalStyles.js
dentro de la carpeta de estilos. Para crear un estilo global en componentes con estilo, necesitamos importar createGlobalStyle
.
import { createGlobalStyle } from "styled-components";
Entonces podemos definir nuestros estilos de la siguiente manera:
export const GlobalStyle = createGlobalStyle` ... `
Los componentes con estilo hacen uso de la plantilla literal para definir accesorios. Dentro de este literal, podemos escribir nuestros códigos CSS
tradicionales.
También deviceWidth
definido en un archivo llamado definition.js
. El deviceWidth
del dispositivo contiene la definición de puntos de interrupción para configurar nuestras consultas de medios.
import { deviceWidth } from "./definition";
Establecemos el desbordamiento en oculto para controlar el flujo de nuestra aplicación.
html, body{ overflow-x: hidden; }
También definimos el estilo del encabezado usando el selector 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%; } ... }
Aquí, se definen varios estilos, como el color de fondo, el índice z, el relleno y muchas otras propiedades tradicionales de CSS.
Hemos utilizado los props
de componentes con estilo para establecer el color de fondo. Esto nos permite establecer variables dinámicas que se pueden pasar desde nuestro componente. Además, también pasamos la variable del tema para permitirnos aprovechar al máximo nuestro cambio de tema.
La creación de temas es posible aquí porque hemos envuelto toda nuestra aplicación con el ThemeProvider
de los componentes con estilo. Hablaremos de esto en un momento. Además, usamos el CSS flexbox
para diseñar correctamente nuestro encabezado y establecer la posición en fixed
para asegurarnos de que permanezca fijo con respecto al navegador. También definimos los puntos de interrupción para que los encabezados sean aptos para dispositivos móviles.
Aquí está el código completo para nuestro archivo 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}; } `;
Tenga en cuenta que escribimos código CSS puro dentro del literal, pero hay algunas excepciones. Styled-components nos permite pasar accesorios. Puede obtener más información sobre esto en la documentación.
Además de definir estilos globales, podemos definir estilos para páginas individuales.
Por ejemplo, aquí está el estilo de PersonListPage.js
definido en PersonStyle.js
en la carpeta 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; } } } } `;
Primero importamos estilos desde styled-components
styled
deviceWidth
de dispositivo desde el archivo de definition
. Luego definimos PersonsListContainer
como un div
para contener nuestros estilos. Usando consultas de medios y los puntos de interrupción establecidos, hicimos que la página fuera compatible con dispositivos móviles al establecer varios puntos de interrupción.
Aquí, hemos utilizado solo los puntos de interrupción del navegador estándar para pantallas pequeñas, grandes y muy grandes. También aprovechamos al máximo el cuadro flexible y la cuadrícula de CSS para diseñar y mostrar correctamente nuestro contenido en la página.
Para usar este estilo en nuestro archivo PersonListPage.js
, simplemente lo importamos y lo agregamos a nuestra página de la siguiente manera.
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
El contenedor generará un div
porque lo definimos como un div en nuestros estilos.
Agregar temas y envolverlo
Siempre es una característica genial agregar temas a nuestra aplicación. Para ello, necesitamos lo siguiente:
- Nuestros temas personalizados definidos en un archivo separado (en nuestro caso, archivo
definition.js
). - La lógica definida en nuestras acciones y reductores de Redux.
- Llamar a nuestro tema en nuestra aplicación y pasarlo a través del árbol de componentes.
Veamos esto.
Aquí está nuestro objeto de theme
en el archivo 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)" } };
Hemos agregado varias propiedades de color para los temas claros y oscuros. Los colores se eligen cuidadosamente para permitir la visibilidad tanto en modo claro como oscuro. Puedes definir tus temas como quieras. Esta no es una regla dura y rápida.
A continuación, agreguemos la funcionalidad a Redux.
Hemos creado globalActions.js
en nuestra carpeta de acciones de Redux y hemos agregado los siguientes 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)); };
Aquí, simplemente importamos nuestros temas definidos. Despachamos las acciones correspondientes, pasando el payload de los themes que necesitábamos. Los resultados de la carga útil se almacenan en el almacenamiento local utilizando las mismas claves para los temas claros y oscuros. Esto nos permite conservar los estados en el navegador.
También necesitamos definir nuestro reductor para los 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; } };
Esto es muy similar a lo que hemos estado haciendo. Usamos la instrucción switch
para verificar el tipo de acción y luego devolvimos la payload
apropiada. También devolvimos una light
de estado que determina si el usuario selecciona el tema claro u oscuro. Usaremos esto en nuestros componentes.
También necesitamos agregarlo a nuestro reductor de raíz y tienda. Aquí está el código completo para nuestro 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));
Dado que necesitábamos conservar el tema cuando el usuario actualizaba, teníamos que obtenerlo del almacenamiento local usando localStorage.getItem()
y pasarlo a nuestro estado inicial.
Agregando la funcionalidad a nuestra aplicación React
Los componentes con estilo nos proporcionan ThemeProvider
que nos permite pasar temas a través de nuestra aplicación. Podemos modificar nuestro archivo App.js para agregar esta funcionalidad.
Echémosle un vistazo.
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;
Al pasar temas a través de ThemeProvider
, podemos usar fácilmente los accesorios de tema en nuestros estilos.
Por ejemplo, podemos establecer el color de nuestro color personalizado bodyText
de la siguiente manera.
color: ${(props) => props.theme.bodyText};
Podemos usar los temas personalizados en cualquier lugar donde necesitemos color en nuestra aplicación.
Por ejemplo, para definir border-bottom
, hacemos lo siguiente.
border-bottom: 2px solid ${(props) => props.theme.goldish};
Conclusión
Comenzamos profundizando en Sanity.io, configurándolo y conectándolo a nuestra aplicación React. Luego configuramos Redux y usamos el lenguaje GROQ para consultar nuestra API. Vimos cómo conectar y usar Redux a nuestra aplicación React usando react-redux
, usar componentes con estilo y temas.
Sin embargo, solo arañamos la superficie de lo que es posible con estas tecnologías. Lo animo a revisar los ejemplos de código en mi repositorio de GitHub y probar un proyecto completamente diferente usando estas tecnologías para aprender y dominarlas.
Recursos
- Documentación de cordura
- Cómo construir un blog con Sanity.io por Kapehe
- Documentación Redux
- Documentación de componentes con estilo
- Hoja de referencia de GROQ
- Documentación de la interfaz de usuario del material
- Redux Middleware y efectos secundarios
- Documentación del Thunk de Redux