Redux - Un compteur avec Redux et JavaScript (sans React)
Introduction
Redux se définit comme un « conteneur d'état prévisible » (en anglais predictable state container) pour les applications JavaScript.
Il t'aide à écrire des applications qui se comportent de façon cohérente, tournent dans différents environnements (client, serveur, natif) et sont faciles à tester.
Tu peux utiliser Redux avec React, ou avec n'importe quelle autre bibliothèque d'affichage. Il est minuscule (2 ko, dépendances comprises), mais possède un large écosystème d'extensions.
Dans cette quête, on va créer un compteur avec Redux en JavaScript « vanilla » (c'est à dire sans aucune bibliothèque, juste du JavaScript pur).
C'est parti ! 🚀

🤓 À la fin de cette quête, tu comprendras :
- ✅ Ce qu'est Redux
- ✅ Comment utiliser Redux dans un projet simple
1. 🏗️ Créer le projet
Commence par créer un dossier quest-redux-vanillaJs quelque part sur ton ordinateur. Le dossier doit contenir deux fichiers :
- index.html
- main.js
Dans le index.html, crée la structure de base, puis à l'intérieur du body :
- Un paragraphe qui contiendra le compteur (id="counter-render")
- 2 boutons :
- Increment (id:"button-increment") (qui augmentera le compteur de un)
- Decrement (id:"button-decrement") (qui diminuera le compteur de un)
On doit aussi importer la bibliothèque Redux. Pour cet exemple, on va utiliser un CDN (Content Delivery Network : un réseau de serveurs qui héberge des fichiers pour les charger rapidement depuis une adresse).
Tu trouveras un lien vers un CDN sur cette page : https://cdnjs.com/libraries/redux.
Choisis la version qui contient « .min » (une version minifiée, c'est à dire compressée, du fichier) et copie/colle le lien dans une balise script.
Enfin, importe notre main.js avec une balise script.
Show me the code!|||html|||Problems figuring it out? |||0|||Hide
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="counter-render"></p>
<button id="button-increment">Increment</button>
<button id="button-decrement">Decrement</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="./main.js" defer></script>
</body>
</html>
On est prêts à y aller !
2. 🧮 Les reducers
Un reducer est une fonction qui prend l'état actuel (le state) et une action comme arguments, et renvoie un nouvel état en résultat.
Voyons un exemple vraiment basique :
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
counterReducer(0, {type:'INCREMENT'});
Ici, la fonction reducer counterReducer accepte deux arguments :
- Un état (state)
- Une action
À l'intérieur de la fonction, on regarde le type de l'action, et on renvoie une valeur modifiée en conséquence.
⏱️ Prends 5 min pour regarder le code et essayer de le comprendre.
À ton avis, que va renvoyer cet appel de fonction ?
🔬 Expérimente :
Essaie par toi-même, dans le main.js : reprends le code précédent et fais un console.log() du résultat de counterReducer avec différentes valeurs.
Que vois-tu ?
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// REDUCER
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
console.log(counterReducer(0, {type:'INCREMENT'}));
// 1
console.log(counterReducer(4, {type:'DECREMENT'}));
// 3
console.log(counterReducer(8, {type:'INCREMENT'}));
// 9
3. ↪️ Créer des types d'action
Dans le code précédent, on a donné l'objet action directement à la fonction.
Mais ce n'est pas très propre. Une chose qu'on pourrait faire, c'est créer des variables pour stocker les différents objets action.
En haut du fichier main.js, écrivons les différents objets action :
const incrementAction = {
type: "INCREMENT"
}
const decrementAction = {
type: "DECREMENT"
}
Maintenant, au lieu d'écrire l'objet à chaque appel de la fonction, on peut utiliser ces objets :
console.log(counterReducer(0, incrementAction));
// 1
À ton tour ! Dans main.js, écris les deux objets action.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
// REDUCER
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
console.log(counterReducer(0, incrementAction ));
// 1
console.log(counterReducer(4, decrementAction ));
// 3
console.log(counterReducer(8, incrementAction ));
// 9
4. 🏬 Créer le store
Dans le code précédent, on n'avait pas vraiment d'état persistant. Ce serait bien d'avoir une valeur qui persiste quand on la met à jour, non ?
Pour ça, on a besoin du store Redux ! Le store va contenir tout l'arbre d'état (state tree) de notre application.
La seule façon de changer l'état sera de dispatcher une action dessus (dispatch : envoyer, déclencher une action).
Créons un store pour notre projet ! Pour cela, dans main.js, utilise la fonction createStore fournie par Redux et passe-lui le reducer en argument.
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
On doit aussi définir quel sera notre état initial. Avant la déclaration de la fonction, on va créer une variable initialState avec la valeur zéro.
On doit également fournir cette valeur à notre fonction reducer, pour qu'à la création du store, la valeur soit initialisée.
const initialState = 0;
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
À ton tour : après le reducer, crée le store. Puis crée un état initial avant la déclaration du reducer, et fournis enfin cet état initial à la fonction reducer.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
// REDUCER
let initialState = 0;
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
Dispatcher une action
Maintenant, il est temps de dispatcher une action !
Si tu fais un console.log sur le store qu'on vient de créer, tu verras qu'on a accès à différentes méthodes :
console.log(store);
/*
dispatch: ƒ y(e)
getState: ƒ p()
replaceReducer: ƒ (e)
subscribe: ƒ h(e)
Symbol(observable): ƒ ()
*/
Tu peux voir que la première méthode s'appelle dispatch.
C'est elle qu'on va utiliser pour dispatcher une action.
store.dispatch(incrementAction);
Tu peux aussi voir qu'on a accès à une méthode appelée getState : elle renvoie la valeur actuelle de l'état.
console.log(store.getState());
🔬 Expérimente :
Essaie par toi-même, dans le main.js : dispatche quelques actions et fais un console.log des résultats.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
let initialState = 0;
// REDUCER
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
store.dispatch(incrementAction);
console.log(store.getState());
// 1
store.dispatch(incrementAction);
console.log(store.getState());
// 2
store.dispatch(incrementAction);
console.log(store.getState());
// 3
store.dispatch(incrementAction);
console.log(store.getState());
// 4
store.dispatch(decrementAction);
console.log(store.getState());
// 3
S'abonner (subscribe)
Dans l'exemple précédent, tu vois qu'on utilise console.log à chaque fois qu'on dispatche une action, ce qui n'est pas DRY (Don't Repeat Yourself : ne te répète pas, un principe qui dit d'éviter de recopier le même code).
Ce serait super de pouvoir créer une fonction avec notre console.log, qui se déclencherait à chaque changement de l'état.
Eh bien, c'est exactement le rôle de la méthode subscribe qu'on obtient à la création du store !
On peut fournir une fonction à store.subscribe ; elle sera déclenchée à chaque changement de l'état.
store.subscribe(myFunction);
🔬 Expérimente :
Crée une fonction appelée render qui fait console.log(store.getState()), puis passe cette fonction à store.subscribe.
Ensuite, utilise dispatch pour voir la magie opérer ✨.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
let initialState = 0;
// REDUCER
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
const render = () => console.log(store.getState());
store.subscribe(render);
store.dispatch(incrementAction);
// 1
store.dispatch(decrementAction);
// 0
store.dispatch(incrementAction);
// 1
5. 🔲 Relier au DOM
Maintenant que toute la logique est en place, relions-la au DOM (Document Object Model : la représentation de la page que le navigateur manipule).
D'abord, créons quelques variables pour nos éléments du DOM.
const counterRender = document.getElementById('counter-render');
const incrementButton = document.getElementById('button-increment');
const decrementButton = document.getElementById('button-decrement');
Ensuite, modifions notre fonction render pour qu'elle remplace le contenu de la balise <p> par l'état.
On peut alors appeler render une première fois pour voir la valeur sur la page.
const render = () => {
counterRender.innerText = store.getState();
};
render();
Puis, ajoutons des écouteurs (listeners) à nos boutons : au clic, on dispatche la bonne action.
incrementButton.addEventListener('click', () =>
store.dispatch(incrementAction)
);
decrementButton.addEventListener('click', () =>
store.dispatch(decrementAction)
);
À ton tour ! Crée les variables qui référencent nos nœuds du DOM, et modifie la fonction render pour afficher l'état dans le <p>.
Ensuite, ajoute les écouteurs qui dispatchent les bonnes actions.
Ton compteur devrait maintenant être pleinement fonctionnel ! Sinon, jette un œil à la solution :
Show me the code!|||codesandbox|||Problems figuring it out? |||0|||Hide
https://codesandbox.io/s/redux-without-react-xdkr6?file=/src/index.js
💪 Challenge
Améliore notre compteur
Maintenant que tu as tout ce qu'il faut pour comprendre Redux et créer un compteur de base, tu vas mettre ça en pratique en améliorant le compteur.
Dans le même fichier, écris trois actions de plus :
incrementByTen(le compteur augmente de 10)decrementByTen(le compteur diminue de 10)reset(le compteur revient à zéro)
Ajoute les cas correspondants à ton reducer et crée trois boutons de plus.
Rends un gist comme solution du challenge (gist : un extrait de code partagé publiquement sur GitHub).
🧐 Critères de validation
- [ ] L'application a 5 boutons différents (
increment,decrement,add 10,remove 10,reset) - [ ] La valeur du compteur s'affiche dynamiquement
- [ ] L'application utilise Redux
- [ ] L'application n'utilise pas React
- [ ] Les fichiers
index.htmletmain.jssont présents et contiennent le code nécessaire au bon fonctionnement de l'application
Introduction
Redux is defined as a "predictable state container" for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.
You can use Redux together with React, or with any other view library. It is tiny (2kB, including dependencies), but has a large ecosystem of add ons available.
In this quest, we are going to create a counter with Redux using vanilla JavaScript.
Let's get started! 🚀

🤓 By the end of this quest, you will understand:
- ✅ What is Redux
- ✅ How to use Redux in a simple project
1. 🏗️ Create the project
Start off by creating a folder quest-redux-vanillaJs somewhere on your computer. The folder should contain two files:
- index.html
- main.js
Inside the index.html, create the basic structure and then create inside the body:
- A paragraph that will contain the counter (id="counter-render")
- 2 Buttons:
- Increment (id:"button-increment") (that will increment the counter by one)
- Decrement (id:"button-decrement") (that will decrement the counter by one)
We need also to import the Redux library. For this example, we will use a CDN.
You can find a link to a CDN on this web page: https://cdnjs.com/libraries/redux.
Choose the version containing ‘.min’, which is a minified version of the file, and copy/paste the link in a script tag.
Finally, import our main.js using a script tag.
Show me the code!|||html|||Problems figuring it out? |||0|||Hide
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="counter-render"></p>
<button id="button-increment">Increment</button>
<button id="button-decrement">Decrement</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="./main.js" defer></script>
</body>
</html>
We are now ready to go!
2. 🧮 Reducers
Reducers are functions that take the current state and an action as arguments and returns a new state result.
Let's see a really basic example:
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
counterReducer(0, {type:'INCREMENT'});
Here, the reducer function counterReducer accepts two arguments:
- A state
- An action
Inside our function, we are checking the action type, and we are returning a modified value according to that.
⏱️ Take 5 min to look at the code and try to understand it.
What do you think will be the return of the function call?
🔬 Experiment:
Try by yourself, in the main.js, use the previous code and console.log() the result of counterReducer with different values.
What do you see?
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// REDUCER
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
console.log(counterReducer(0, {type:'INCREMENT'}));
// 1
console.log(counterReducer(4, {type:'DECREMENT'}));
// 3
console.log(counterReducer(8, {type:'INCREMENT'}));
// 9
3. ↪️ Create some action type
In the previous code, we gave the action object directly to the function.
But it's not really clean. One thing we could do is to create variables to store the different action objects.
At the top of the main.js file, let's write the different action objects:
const incrementAction = {
type: "INCREMENT"
}
const decrementAction = {
type: "DECREMENT"
}
Now, instead of writing the object every time we call the function, we can use those objects:
console.log(counterReducer(0, incrementAction));
// 1
Your turn! In main.js, write the two action objects.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
// REDUCER
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
console.log(counterReducer(0, incrementAction ));
// 1
console.log(counterReducer(4, decrementAction ));
// 3
console.log(counterReducer(8, incrementAction ));
// 9
4. 🏬 Create the store
In the previous code, we didn't really have any persistent state. It would be nice to have a value that persists when we update it, right?
For that, we need to use the Redux Store! The store will hold the whole state tree of our application.
The only way to change the state will be to dispatch an action on it.
Let's create a store for our project! To do so, in main.js use the createStore function provided by Redux and pass the reducer as an argument.
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
Also, we need to define what would be our initial state. Before the function declaration, we will create a variable initialState with the value of zero.
We need also to provide this value to our reducer function so that when we create the store, the value is initialized.
const initialState = 0;
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
Your turn, after the reducer, create the store. Then, create an initial state before the declaration of the reducer and finally provide the initial state to the reducer function.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
// REDUCER
let initialState = 0;
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
Dispatch an action
Now, it's time to dispatch an action!
If you use console.log and look at the store we've just create, you can see that we have access to different methods:
console.log(store);
/*
dispatch: ƒ y(e)
getState: ƒ p()
replaceReducer: ƒ (e)
subscribe: ƒ h(e)
Symbol(observable): ƒ ()
*/
You can see that the first method is called dispatch.
This is what we are going to use to dispatch an action.
store.dispatch(incrementAction);
You can also see that we have access to a method called getState, this method will return the current state value.
console.log(store.getState());
🔬 Experiment:
Try by yourself, in the main.js, dispatch some action console.log the results.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
let initialState = 0;
// REDUCER
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
store.dispatch(incrementAction);
console.log(store.getState());
// 1
store.dispatch(incrementAction);
console.log(store.getState());
// 2
store.dispatch(incrementAction);
console.log(store.getState());
// 3
store.dispatch(incrementAction);
console.log(store.getState());
// 4
store.dispatch(decrementAction);
console.log(store.getState());
// 3
Subscribe
In the previous example, you can see that we are using console.log every time we dispatch an action which is not DRY.
It would be great if we could create a function with our console.log that will be triggered every time the state is changing.
Well, this is exactly the purpose of the subscribe method that we've got when we create the store!
We can provide a function to store.subscribe that will be triggered every time the state change.
store.subscribe(myFunction);
🔬 Experiment:
Create a function called render that console.log(store.getState()), then, pass this function to store.subscribe.
Then, use dispatch to see the magic ✨.
Show me the code!|||javascript|||Problems figuring it out? |||0|||Hide
// ACTIONS
const incrementAction = {
type: 'INCREMENT',
};
const decrementAction = {
type: 'DECREMENT',
};
let initialState = 0;
// REDUCER
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// STORE
const { createStore } = Redux;
const store = createStore(counterReducer);
const render = () => console.log(store.getState());
store.subscribe(render);
store.dispatch(incrementAction);
// 1
store.dispatch(decrementAction);
// 0
store.dispatch(incrementAction);
// 1
5. 🔲 Connect with the DOM
Now that all the logic is implemented, let's connect it to the DOM.
First, let's create some variables for our DOM elements.
const counterRender = document.getElementById('counter-render');
const incrementButton = document.getElementById('button-increment');
const decrementButton = document.getElementById('button-decrement');
Then, let's change our render function so that it replaces the content inside the <p> tag by the state.
We can then call render the first time so that we can see the value on the page.
const render = () => {
counterRender.innerText = store.getState();
};
render();
Then, let's add some listeners to our button, and on click, dispatch the right action.
incrementButton.addEventListener('click', () =>
store.dispatch(incrementAction)
);
decrementButton.addEventListener('click', () =>
store.dispatch(decrementAction)
);
Your turn! Create the variables that reference our DOM nodes and change the render function to render the state in the <p>.
Then, add the listeners dispatching the right actions.
Your counter should now be fully working! If not, take a look at the solution:
Show me the code!|||codesandbox|||Problems figuring it out? |||0|||Hide
https://codesandbox.io/s/redux-without-react-xdkr6?file=/src/index.js
💪 Challenge
Improve our counter
Now that you have everything you need to understand Redux and create a basic counter, you are going to put this into practice by improving the counter.
Using the same file, write three more actions:
incrementByTen(the counter increment by 10)decrementByTen(the counter decrement by 10)reset(the counter go back to zero)
Add the cases to your reducer and create three more buttons.
Submit a gist as solution of the challenge
🧐 Validation criteria
- [ ] The application has 5 different buttons (
increment,decrement,add 10,remove 10,reset) - [ ] The counter value is displayed dynamically
- [ ] The application implements Redux
- [ ] The application does not use React
- [ ] The
index.htmlandmain.jsfiles are present and contain the necessary code for the application to operate normally
Ressources partagées sur cette quête
- Pour remettre en contexte ce qu'est Redux et pourquoi l'utiliserhttps://medium.com/@aymenzaouali/pourquoi-utiliser-redux-f38b55430d13