Uso del plugin de Redux para capturar cambios de estado
Video Tutorial
Mira cómo rastrear el estado con Redux
Si no te gusta leer, puedes seguir este tutorial en video que te muestra cómo rastrear el estado de tus aplicaciones React cuando usas Redux
Si necesitas más visibilidad al reproducir las sesiones de tus usuarios, echar un vistazo al estado de la aplicación puede ser muy útil.
En el caso de Redux, OpenReplay proporciona un plugin que te permite integrarte con el funcionamiento interno del store. Este plugin te dejará ver el estado (state) del store de Redux y las acciones despachadas a lo largo de la sesión grabada.
Una vez configurado, deberías poder observar los cambios en el store como se ve en la siguiente captura de pantalla:

Configuración de Redux en un proyecto Next.js
Section titled Configuración de Redux en un proyecto Next.jsPara este tutorial, usaremos este repositorio (rama redux-store) de un sitio genérico de comercio electrónico construido con Next.js.
En este proyecto, reemplazaremos un conjunto de productos destacados por un conjunto de productos nuevos obtenidos de una API externa.
Para ello, añadiremos una función que solicite los productos usando Axios, y lo haremos desde dentro de una acción de Redux.
Nota: Esta es una aplicación Next.js compleja, por lo que es posible que no siga la estructura estándar que se encuentra en las clásicas apps de tareas (To-Do), pero siguiendo este tutorial deberías poder seguir el ritmo de los cambios.
Recuerda: siempre puedes clonar el repositorio y revisar el código tú mismo.
Comenzaremos instalando todas las dependencias principales con:
npm i next-redux-wrapper redux react-redux redux-thunk redux-devtools-extension
Hecho esto, crea una carpeta llamada store en el directorio raíz de tu proyecto y reproduce la siguiente estructura:

El archivo types.js contendrá la definición de tipos para las dos acciones que vamos a definir:
export const GET_PRODUCTS = 'GET_PRODUCTS'
export const PRODUCTS_ERROR = 'PRODUCTS_ERROR'
El archivo store.js exportará una función que, CUANDO se llame, creará un nuevo store de Redux. Esto se debe a que necesitaremos añadir un nuevo middleware de Redux devuelto por el plugin de Redux (más sobre esto en un momento).
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducers'
const initalState = {}
export default function createReduxStore(extraMiddleware = []) {
const middleware = [thunk, ...extraMiddleware]
const store = createStore(
rootReducer,
initalState,
composeWithDevTools(applyMiddleware(...middleware))
)
return store
}
Nuestro archivo reducer (makeUpReducer.js) actualizará el estado ya sea con la lista de productos o con el mensaje de error devuelto cuando hay un problema.
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
const initialState = {
makeUpProducts: [],
loading: true,
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_PRODUCTS:
return {
...state,
makeUpProducts: action.payload,
loading: false,
}
case PRODUCTS_ERROR:
return {
loading: false,
error: action.payload,
}
default:
return state
}
}
Y finalmente, el archivo de acciones definirá una sola función, encargada de obtener la lista de productos desde una API externa y despachar la acción y el payload correctos:
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
import axios from 'axios'
import slugify from 'slugify'
export const getMakeUpProducts = () => async (dispatch: any) => {
console.log('Getting the makeup products')
try {
let { data } = await axios.get(
'https://makeup-api.herokuapp.com/api/v1/products.json?brand=maybelline&apiKey=123fff132'
)
const products = data
let newProds = products.map((p: any) => {
return {
id: '' + p.id,
slug: slugify(p.name),
name: p.name,
description: '',
images: [{ url: p.image_link }],
variants: [],
price: {
value: +p.price,
},
options: [],
}
})
dispatch({
type: GET_PRODUCTS,
payload: newProds,
})
} catch (e) {
dispatch({
type: PRODUCTS_ERROR,
payload: e,
})
}
}
Configuración del proveedor del tracker
Section titled Configuración del proveedor del trackerPara inyectar el tracker en la aplicación, usaremos un contexto proporcionado como se muestra en el tutorial de Next.js.
Este proveedor te permitirá configurar un conjunto de plugins; en nuestro caso, usaremos el plugin de Redux, así (desde dentro del archivo _app.tsx)
//...more imports here....
import TrackerProvider from '../context/trackerProvider'
import trackerRedux from '@openreplay/tracker-redux'
// ... more code here....
export default function MyApp({ Component, pageProps }: AppProps) {
const Layout = (Component as any).Layout || Noop
useEffect(() => {
document.body.classList?.remove('loading')
}, [])
let plugins = [
{
fn: trackerRedux,
name: 'redux',
config: {},
},
]
return (
<TrackerProvider config={{ plugins }}>
<Head />
<ManagedUIContext>
<Layout pageProps={pageProps}>
<Component {...pageProps} />
</Layout>
</ManagedUIContext>
</TrackerProvider>
)
}
Ahora bien, este código te permite configurar el tracker con el plugin correcto, pero para que el plugin funcione, necesitaremos acceder al middleware devuelto cuando se llama al plugin. Eso significa que tendremos que llevar un registro de los valores devueltos por nuestros plugins para poder usarlos en otra parte. En este caso, necesitaremos usarlo al llamar a la función createReduxStore de arriba.
Para hacerlo, tenemos que extender el TrackerProvider para asegurarnos de conservar el valor devuelto dentro del estado, así (puedes revisar la versión completa de este archivo aquí):
import { createContext, useCallback } from 'react'
import Tracker from '@openreplay/tracker'
import { v4 as uuidV4 } from 'uuid'
import { useReducer } from 'react'
export const TrackerContext = createContext()
function defaultGetUserId() {
return uuidV4()
}
function newTracker(config) {
///code here
}
function reducer(state, action) {
switch (action.type) {
case 'init': {
if (!state.tracker) {
console.log('Instantiaing the tracker for the first time...')
let t = newTracker(state.config)
let pluginsReturnedValue = {}
if (state.config.plugins) {
state.config.plugins.forEach((p) => {
console.log('Using plugin...')
pluginsReturnedValue[p.name] = t.use(p.fn(p.config)) //keep track
})
}
return {
...state,
pluginsReturnedValue: pluginsReturnedValue, //update the state
tracker: t,
}
}
return state
}
case 'start': {
console.log('Starting tracker...')
state.tracker.start()
return state
}
}
}
export default function TrackerProvider({ children, config = {} }) {
let [state, dispatch] = useReducer(reducer, {
tracker: null,
pluginsReturnedValue: {},
config,
})
let value = {
startTracking: () => dispatch({ type: 'start' }),
initTracker: () => dispatch({ type: 'init' }),
pluginsReturnedValues: { ...state.pluginsReturnedValue }, //inject the state
}
return (
<TrackerContext.Provider value={value}>{children}</TrackerContext.Provider>
)
}
Dentro de la acción init, también llevamos un registro de los valores devueltos por el método use cuando se llama con nuestros plugins. Y mantenemos ese diccionario dentro de la propiedad state.pluginsReturnedValue. La cual ponemos a disposición de todos los hijos a través de la variable pluginsReturnedValues.
Esa lógica te permite usar el plugin al inicializar el tracker y luego acceder al middleware y usarlo más tarde.
Creación del store de Redux con el nuevo middleware
Section titled Creación del store de Redux con el nuevo middlewareAhora que tenemos el plugin funcionando, necesitamos inicializar el store de Redux, y debemos hacerlo después de que el Tracker se haya inicializado y antes de que se llame al método start.
Para ello, he elegido el componente ManagedUI, que se usa directamente en el archivo _app.tsx. Este componente está envuelto por nuestro Tracker Provider, lo que significa que tendrá acceso al contexto que estamos compartiendo.
El componente se ve así:
export const ManagedUIContext: FC = ({ children }) => {
const { initTracker, pluginsReturnedValues } = useContext(TrackerContext)
const [store, setStore] = useState<Store>()
useEffect(() => {
initTracker()
}, [])
useEffect(() => {
if (!pluginsReturnedValues['redux']) return
let middleWares = pluginsReturnedValues['redux']
? [pluginsReturnedValues['redux']]
: []
setStore(createReduxStore(middleWares))
}, [pluginsReturnedValues])
return (
<div>
{store && (
<Provider store={store}>
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
</Provider>
)}
</div>
)
}
Los puntos clave de este archivo son:
- Estamos obteniendo la función
initTrackery el atributopluginsReturnedValuesdel contexto. - Llamamos a la primera solo una vez, cuando el componente se monta (a través del primer
useEffect). - Luego creamos el store de Redux solo una vez que la variable
pluginsReturnedValuestiene nuestro valor devuelto. El segundouseEffectse llamará dos veces, una cuando la página carga y luego cuando el métodoinitTrackermodifica nuestra variable de estado. La segunda vez, crearemos el store con el middleware almacenado enpluginsReturnedValues.
Inicio del tracker
Section titled Inicio del trackerCon el plugin configurado y el store de Redux correctamente creado, todo lo que tenemos que hacer ahora es llamar al método start del tracker.
La lógica para esto se añadirá en el archivo index.tsx, y puedes echar un vistazo al código fuente completo de este archivo aquí.
La parte relevante de este código que necesitaremos revisar es la siguiente:
// imports and more logic goes here...
export default function Home({
products,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { startTracking } = useContext(TrackerContext)
const dispatch = useDispatch()
const makeUpProductsList = useSelector((state: any) => state.makeUpProducts)
const { makeUpProducts } = makeUpProductsList
useEffect(() => {
async function getProds() {
await startTracking()
dispatch(getMakeUpProducts() as any)
}
getProds()
}, [dispatch])
return (
<>
<Grid variant="filled">
{products.slice(0, 3).map((product: any, i: number) => (
<ProductCard
key={product.id}
product={product}
imgProps={{
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540,
priority: true,
}}
/>
))}
</Grid>
<Marquee variant="secondary">
{makeUpProducts.slice(0, 3).map((product: any, i: number) => (
<ProductCard key={product.id} product={product} variant="slim" />
))}
</Marquee>
<!-- more code here -->
</>
)
}
Solo vamos a usar la función startTracking del proveedor de contexto del Tracker y el hook useSelector de Redux para capturar la lista de productos devueltos.
El hook useEffect desencadenará la llamada a startTracking y la obtención de la nueva lista de productos de maquillaje al despachar la llamada a la función getMakeUpProducts.
Y con eso, deberías poder desplegar tu aplicación (siempre que hayas configurado la API key como se menciona en este tutorial)
¿Tienes preguntas?
Section titled ¿Tienes preguntas?Puedes consultar este repositorio para ver el código fuente completo de una aplicación funcional basada en Next.js con un store de Redux.
Si tienes algún problema al configurar el plugin de Redux, ¡contáctanos en nuestra comunidad de Slack y pregunta directamente a nuestros desarrolladores!