Archives Wild Code School
JavaScript - Beyond the Basics

Redux - Un compteur avec Redux et JavaScript (sans React)

Quête 12 sur 12 · 1 image archivée

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 ! 🚀

image

🤓 À 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.

hidden
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 :

javascript
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 ?

hidden
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 :

javascript
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 :

javascript
console.log(counterReducer(0, incrementAction));
// 1

À ton tour ! Dans main.js, écris les deux objets action.

hidden
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.

javascript
// 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.

javascript
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.

hidden
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 :

javascript
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.

javascript
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.

javascript
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.

hidden
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.

javascript
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 ✨.

hidden
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.

javascript
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.

javascript
const render = () => {
	counterRender.innerText = store.getState();
};
render();

Puis, ajoutons des écouteurs (listeners) à nos boutons : au clic, on dispatche la bonne action.

javascript
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 :

hidden
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.html et main.js sont 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! 🚀

image

🤓 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.

hidden
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:

javascript
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?

hidden
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:

javascript
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:

javascript
console.log(counterReducer(0, incrementAction));
// 1

Your turn! In main.js, write the two action objects.

hidden
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.

javascript
// 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.

javascript
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.

hidden
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:

javascript
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.

javascript
store.dispatch(incrementAction);

You can also see that we have access to a method called getState, this method will return the current state value.

javascript
console.log(store.getState());

🔬 Experiment:

Try by yourself, in the main.js, dispatch some action console.log the results.

hidden
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.

javascript
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 ✨.

hidden
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.

javascript
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.

javascript
const render = () => {
	counterRender.innerText = store.getState();
};
render();

Then, let's add some listeners to our button, and on click, dispatch the right action.

javascript
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:

hidden
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.html and main.js files are present and contain the necessary code for the application to operate normally

Ressources partagées sur cette quête