Utiliser le plugin Redux pour capturer les changements d'état
Video Tutorial
Découvrez comment suivre l'état avec Redux
Si vous n'aimez pas lire, vous pouvez suivre ce tutoriel vidéo qui vous montre comment suivre l'état de vos applications React lorsque vous utilisez Redux
Si vous avez besoin de plus de visibilité lors du rejeu des sessions de vos utilisateurs, pouvoir jeter un œil à l’état de l’application peut s’avérer très utile.
Dans le cas de Redux, OpenReplay fournit un plugin qui vous permet de vous intégrer au fonctionnement interne du store. Ce plugin vous permettra de voir l’état du store Redux ainsi que les actions dispatchées tout au long de la session enregistrée.
Une fois configuré, vous devriez pouvoir observer les changements dans le store comme le montre la capture d’écran suivante :

Configurer Redux sur un projet Next.js
Section titled Configurer Redux sur un projet Next.jsPour ce tutoriel, nous utiliserons ce dépôt (branche redux-store) d’un site e-commerce générique construit avec Next.js.
Dans ce projet, nous remplacerons un ensemble de produits mis en avant par un nouvel ensemble de produits récupérés depuis une API externe.
Pour cela, nous ajouterons une fonction permettant de demander les produits à l’aide d’Axios, et nous le ferons depuis une action Redux.
Remarque : Il s’agit d’une application Next.js complexe, qui peut donc ne pas suivre la structure standard que l’on trouve dans les applications de liste de tâches (To-Do) classiques, mais en suivant ce tutoriel vous devriez pouvoir suivre les changements.
Souvenez-vous : vous pouvez toujours cloner le dépôt et examiner le code vous-même.
Nous commencerons par installer toutes les dépendances principales avec :
npm i next-redux-wrapper redux react-redux redux-thunk redux-devtools-extension
Cela étant fait, créez un dossier appelé store à la racine de votre projet et reproduisez la structure suivante :

Le fichier types.js contiendra la définition de type pour les deux actions que nous allons définir :
export const GET_PRODUCTS = 'GET_PRODUCTS'
export const PRODUCTS_ERROR = 'PRODUCTS_ERROR'
Le fichier store.js exportera une fonction qui, LORSQU’elle est appelée, créera un nouveau store Redux. C’est parce que nous devrons ajouter un nouveau middleware Redux renvoyé par le plugin Redux (nous y reviendrons dans un instant).
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
}
Notre fichier reducer (makeUpReducer.js) mettra à jour l’état soit avec la liste des produits, soit avec le message d’erreur renvoyé en cas de problème.
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
}
}
Et enfin, le fichier d’action définira une seule fonction, chargée de récupérer la liste des produits depuis une API externe et de dispatcher la bonne action ainsi que le bon payload :
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,
})
}
}
Configurer le fournisseur du tracker
Section titled Configurer le fournisseur du trackerCe fournisseur vous permettra de configurer un ensemble de plugins ; dans notre cas, nous utiliserons le plugin Redux, de cette manière (depuis le fichier _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>
)
}
À présent, ce code vous permet de configurer le tracker avec le bon plugin, mais pour que le plugin fonctionne, nous devrons accéder au middleware renvoyé lorsque le plugin est appelé. Cela signifie que nous devrons garder une trace des valeurs renvoyées par nos plugins afin de pouvoir les utiliser ailleurs. Dans ce cas, nous devrons l’utiliser lors de l’appel à la fonction createReduxStore ci-dessus.
Pour ce faire, nous devons étendre le TrackerProvider afin de nous assurer de conserver la valeur renvoyée dans l’état, de cette manière (vous pouvez consulter la version complète de ce fichier ici) :
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>
)
}
À l’intérieur de l’action init, nous gardons également une trace des valeurs renvoyées par la méthode use lorsqu’elle est appelée avec nos plugins. Et nous conservons ce dictionnaire dans la propriété state.pluginsReturnedValue. Que nous rendons accessible à tous les composants enfants via la variable pluginsReturnedValues.
Cette logique vous permet d’utiliser le plugin lors de l’initialisation du tracker, puis d’accéder au middleware et de l’utiliser plus tard.
Créer le store Redux avec le nouveau middleware
Section titled Créer le store Redux avec le nouveau middlewareMaintenant que notre plugin fonctionne, nous devons initialiser le store Redux, et nous devons le faire après que le Tracker a été initialisé et avant que la méthode start ne soit appelée.
Pour cela, j’ai choisi le composant ManagedUI, qui est utilisé directement dans le fichier _app.tsx. Ce composant est enveloppé par notre Tracker Provider, ce qui signifie qu’il aura accès au contexte que nous partageons.
Le composant ressemble à ceci :
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>
)
}
Les points clés à retenir de ce fichier sont les suivants :
- Nous récupérons la fonction
initTrackeret l’attributpluginsReturnedValuesdepuis le contexte. - Nous n’appelons la première qu’une seule fois, lorsque le composant est monté (via le premier
useEffect). - Nous créons ensuite le store Redux uniquement une fois que la variable
pluginsReturnedValuescontient notre valeur renvoyée. Le seconduseEffectsera appelé deux fois, une fois lors du chargement de la page puis lorsque la méthodeinitTrackermodifie notre variable d’état. La deuxième fois, nous créerons le store avec le middleware stocké danspluginsReturnedValues.
Démarrer le tracker
Section titled Démarrer le trackerLe plugin étant configuré et le store Redux correctement créé, il ne nous reste plus qu’à appeler la méthode start du tracker.
La logique correspondante sera ajoutée dans le fichier index.tsx, et vous pouvez consulter le code source complet de ce fichier ici.
La partie pertinente de ce code que nous devrons examiner est la suivante :
// 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 -->
</>
)
}
Nous allons uniquement utiliser la fonction startTracking du fournisseur de contexte du Tracker et le hook useSelector de Redux pour capturer la liste des produits renvoyés.
Le hook useEffect déclenchera l’appel à startTracking ainsi que la récupération de la nouvelle liste de produits de maquillage en dispatchant l’appel à la fonction getMakeUpProducts.
Des questions ?
Section titled Des questions ?Vous pouvez consulter ce dépôt pour obtenir le code source complet d’une application fonctionnelle basée sur Next.js avec un store Redux.
Si vous rencontrez des problèmes lors de la configuration du plugin Redux, contactez-nous sur notre communauté Slack et posez vos questions directement à nos développeurs !