Gérer l’état global d’une application

Au cours d’articles précédents, nous avons réimplémenté de façon naïve la librairie Redux, une librairie majeure dans l’écosystème React. Majeure car elle implémente de façon élégante le pattern flux développé par Facebook et qu’elle possède des features très sympas comme le time traveling (permettant de refaire l’historique des modifications du store Redux) associé au Redux devTool offrant une très bonne developer experience!

Il existe pas mal d’alternatives comme MobX, Unstated ou même - dans une certaine mesure - l’API context de React 16.3 qui est maintenant officielle !

Mais ici, nous allons voir comment pousser un peu plus la logique du state pour avoir une application qui possède bel et bien un state global avec… l’URL de l’application !








URL FTW !

Dans les Single Page Application, contrairement aux applications client/serveur classique, l’URL d’accès n’a pas beaucoup d’importance. Une fois que l’on arrive sur l’application, le javascript s’exécute et le navigateur ne va jamais se refraîchir. Toutes les interactions se passeront sur cette page. Le serveur ne sera appelé que via des requêtes AJAX.

Le fait d’arriver sur https://monApp.fr ou https://monApp.fr?user=gael&job=dev ne change rien dans le cas d’une SPA qui n’utilise pas l’URL.

Pourtant, l’URL peut être utilisée comme une sauvegarde de l’état de notre application où chacune des mises à jour de l’URL va correspondre à une mise à jour de ce même état

Dans ces conditions, le fait d’arriver sur https://monApp.fr ou https://monApp.fr?user=gael&job=dev aura un impact. Cela permettra d’ouvrir l’application dans un état particulier.








Du code pour les braves !

J’ai commencé à implémenter cette idée sur un side project que j’ai développé dont voici le repo Github et le lien pour jouer avec :)

Dans le cadre de cet article, j’ai cependant redéveloppé un codesandbox pour avoir un code minimaliste pour se concentrer sur le sujet de cet article uniquement.

Tout d’abord, qu’est-ce qui nous intéresse dans l’URL ? Toute la partie de l’URL après le ?.
Ce qu’on appelle les search params.

Un peu de doc par ici sur le sujet.




Récupérer des éléments de l’URL

Pour notre exemple, nous n’allons manipuler qu’un seul paramètre dans l’URL : query.

Pour le récupérer (si tant est qu’il soit effectivement présent dans l’URL), on va utiliser une propriété de l’objet window.location.search qui correspond à toute la chaîne de caractères après le ?.

Ainsi avec l’url suivante: www.first-contrib?query=react, on obtient :

console.log(window.location.search); // "?query=react"

Idéalement, plutôt qu’un string, il serait préférable de manipuler un objet correctement formatté. Pour cela, on va utiliser l’objet URLSearchParams accessible dans les navigateurs récents. Si non, vous pouvez le polyfiller via cette librairie par exemple.

En code cela donne cela :

1
2
3
4
5
6
function getParams(location) {
const searchParams = new URLSearchParams(location.search);
return {
query: searchParams.get('query') || '',
};
}

ainsi,

1
2
3
4
5
6
//getParams declaration
const params = getParams('www.first-contrib.fr?query=react');
console.log(params) // { query: "react" }

Parfait, on peut récupérer un objet depuis notre URL. Maintenant comment faire fonctionner ça avec notre application qui intègre la librairie react router pour la gestion du routing ?

On va tenter d’afficher l’objet que l’on obtient depuis l’URL en créant un router qui affichera un composant prenant pour props les éléments de l’URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import { render } from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
// ...
// getParams code above
const MainPage = (props) => {
//on ne sais pas encore d'où query va venir...
let query = '';
return (
<h2>{`Ma query : ${query}`}</h2>
);
}
const App = () => (
<React.Fragment>
<Router>
<React.Fragment>
<Route path="/" component={MainPage} />
</React.Fragment>
</Router>
</React.Fragment>
);
render(<App />, document.getElementById("root"));

En l’état, on a bien un router qui affiche le composant MainPage à l’URL “/“ mais on ne sais pas encore récupérer la valeur de query éventuellement présente dans l’URL.

Pour obtenir query, on va devoir jouer avec la fonction getParams que l’on a créé précédemment ainsi que les props que le composant MainPage reçoit de façon implicite via le composant Route à cette ligne : <Route path="/" component={MainPage} />

Si on logge l’objet props que reçoit MainPage, on voit qu’il ressemble à ça :

{match: Object, location:Object, history: Object, /*d'autres valeurs */}

et Ô miracle, ce props possède l’objet location, similaire au window.location qu’on a manipulé tout à l’heure ! Ainsi, on peut récupérer les valeurs de l’URL dans MainPage. On va donc mettre à jour le code de MainPage en conséquence:

1
2
3
4
5
6
7
8
const MainPage = (props) => {
const { location } = props;
const { query } = getParams(location);
return (
<h2>{`Ma query: ${query}`}</h2>
);
}

Maintenant, le composant MainPage rend des éléments en fonction de l’URL!

Question subsidiare : Quel est l’intérêt de manipuler location à travers react router s’il existe déjà dans l’objet window comme on a vu précédemment ?

Et bien, avec React les composants réagissent à des modifications de state interne ou de props externes. React Router, lui, permet de relier l’état de l’app à l’URL du navigateur. Ainsi, une modification de l’URL lance une mise à jour de l’application.




Mettre à jour l’URL (et donc le state) !

Maintenant que l’on est capable de lire l’URL du navigateur, on va tenter de la mettre à jour depuis notre application.

On va créer un composant qui va modifier l’URL en fonction de la valeur d’un input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class InputPage extends React.Component {
state = { inputValue: "" };
updateInputValue = e => this.setState({ inputValue: e.target.value });
render() {
return (
<React.Fragment>
<input
type="text"
placeholder="Query à modifier"
value={this.state.inputValue}
onChange={this.updateInputValue}
/>
<input type="button" value="Mettre à jour l'URL" onClick={null} />
</React.Fragment>
);
}
}

En l’état, ce composant se contente d’afficher une valeur mais est incapable de modifier l’URL. Ce que l’on souhaite, c’est que l’évènement click sur le bouton récupère les valeurs de l’input et mette à jour l’URL en conséquence. On va donc devoir écrire la fonction du onClick de l’input de type button.

Pour rappel, on a vu plus haut que l’objet props que reçoit MainPage ressemble à ça :

{match: Object, location:Object, history: Object, /*d'autres valeurs */}

Plus précisément, il contient un objet history décrit dans la documentation de React Router ici

Ici, nous allons tout particulièrement nous intéresser à la fonction push qui selon la doc de React Router :

Pushes a new entry onto the history stack

Autrement dit, push va permettre de mettre à jour l’URL !

Seulement, si la valeur query vaut “javacript” par exemple, il faut que la mise à jour de l’URL donne www.monApp.fr?query=javascript. Il faut donc que l’on génère les searchParams de notre nouvelle URL. Pour ça, l’objet URLSearchParams va nous aider encore une fois !

Après la fonction getParams on va donc écrire la fonction setParams.

1
2
3
4
5
function setParams({ query }) {
const searchParams = new URLSearchParams();
searchParams.set("query", query || "");
return searchParams.toString();
}

Ainsi, si on utilise la fonction comme suit:

1
2
const url = setParams({ query: "javascript" });
console.log(url); // "query=javascript"

Maintenant, dans notre input, on va modifier la fonction onClick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class InputPage extends React.Component {
state = { inputValue: "" };
updateInputValue = e => this.setState({ inputValue: e.target.value });
//NOTRE FONCTION !
updateURL = () => {
const url = setParams({ query: this.state.inputValue });
//ne pas oublier le "?" devant les "search params" !
this.props.history.push(`?${url}`);
};
render() {
return (
<React.Fragment>
<input
type="text"
placeholder="Query à modifier"
value={this.state.inputValue}
onChange={this.updateInputValue}
/>
<input
type="button"
value="Mettre à jour l'URL"
onClick={this.updateURL} />
</React.Fragment>
);
}
}

Maintenant, si on change la valeur de l’input et que l’on clique sur le bouton, on a bien l’URL qui se met à jour ainsi que le composant MainPage qui affiche la nouvelle valeur !

Une des choses vraiment sympas avec le stockage de l’état de l’app dans l’URL, c’est le copier/coller de lien. Avec toutes les infos déjà présentes dans l’URL, l’application affiche directement l’état dans lequel on souhaite la trouver ! Essayez en changeant la valeur de l’URL dans le navigateur !

Dans le cas d’un moteur de recherche, on peut lancer la recherche dès le chargement de l’application par exemple. Dans mon application j’utilise react-apollo mais de façon naïve, on peut implémenter la même chose avec n’importe quel client HTTP.

Essayons ici avec la librairie Axios et l’API Github REST qui ne nécessite pas d’authentification pour fonctionner de créer un composant qui lance des requêtes des éléments en fonction des props qu’ils reçoient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const httpClient = axios.create({
baseURL: "https://api.github.com"
});
class ResultsPage extends React.Component {
state = { results: [], loading: false, error: false };
componentDidMount() {
return this.searchRepositories(this.props.query);
}
componentWillReceiveProps(nextProps) {
if (nextProps.query !== this.props.query) {
this.setState({ query: nextProps.query });
return this.searchRepositories(nextProps.query);
}
}
searchRepositories = query => {
if (!query) {
//si la valeur query est nulle, on ne
//lance pas la requête
return this.setState({
results: []
});
}
this.setState({ loading: true, error: false });
return httpClient
.get(`/search/repositories?q=${query}`)
.then(({ data }) =>
this.setState({
results: data.items,
loading: false
})
)
.catch(e => this.setState({ loading: false, error: true }));
};
render() {
return (
<div>
{this.state.results.map(repo => (
<div>
<a key={repo.id} href={repo.html_url}>
{repo.name}
</a>
<div>{`by ${repo.owner.login}`}</div>
</div>
))}
</div>
);
}
}

Ce composant ResultsPage récupère le props query que l’on récupère depuis l’URL (je crois qu’on a compris depuis le temps) et lance directement une requête au montage du composant avec le lifecycle componentDidMount ainsi qu’à la mise à jour de la valeur de query dans avec componentWillReceiveProps. Et, tadam, on a un composant qui requête automatiquement des valeurs au changement de la valeur de l’URL !!

Pour rappel un codesandbox est là pour tester en live.

Dans notre cas, nous n’avons qu’une valeur mais cela devient très sympa si vous voulez gérer la pagination, un filter sur votre recherche, le tri descendant ou ascendant, …, bref tous les paramètres qu’acceptent votre API !

Dans tous les cas, le fonctionnement est le même, les éléments qui modifient le state modifient l’URL qui se propagent dans tous les enfants pour arriver au composant qui effectue la requête !








Conclusion

Voilà, c’est tout pour moi ! React et le state, un univers impitoyable mais dans lequel il y a la possibilité des choses à faire pour s’amuser et fournir des expériences utilisateurs sympas et différentes.

J’espère que cet article vous aura plus, n’hésitez pas à laisser des commentaires.