Naviguer dans son application React

Publié le 12 décembre 2016
#react# react lecon# redux

Une application React c'est cool, mais pouvoir naviguer entre plusieurs pages c'est mieux non ?

Attention, cet article n'est pas à jour.

Vous pourrez retrouver les sources de la leçon précédente à l'adresse suivante : github lecon 5

1. Rappel de l'organisation du projet

src
|**_actions
| |
| |_**actionsTypes.js
| |
| |**_appActions.js
| |
| |_**establishmentActions.js
|
|**_assets
| |
| |_**logo.svg
|
|**_components
| |
| |_**establishments
| | |
| | |**_establishments.js
| | |
| | |_**fixtures.js
| |
| |**_App.js
|
|_**containers
| |
| |**_appContainer.js
| |
| |_**establishmentContainer.js
|
|**_css
| |
| |_**App.css
| |
| |**_index.css
|
|_**reducers
| |
| |**_appReducer.js
| |
| |_**establishmentReducer.js
| |
| |**_index.js
|
|_**App.test.js
|
|\_\_\_index.js

On n'oublie pas de lancer l'application afin de voir les changements dans notre navigateur.

$ cd HappyDrink
$ npm start

2. Routing

Jusqu'à maintenant, notre application se cantonnait à une seul page. Mais on aimerait bien qu'au clique sur le lien "Voir l'happy-hour" d'un établissement, l'application nous dirige vers une nouvelle page, d'url /happyhour/[id de notre etablissement] et nous affiche l'happy-hour correspondante.

Voyons comment implémenter cela.

react-router et react-router-redux

Cette fois-ci, nous allons utiliser les packages npm react-router et react-router-redux:

$ npm install --save react-router
$ npm install --save react-router-redux

Le premier nous permet de synchroniser notre interface React avec l'url ( exactement ce que l'on veut \o/ ). Le second, quant à lui, nous permet de synchroniser tout cela avec Redux.

Implémentation

Nous allons avoir besoin de modifier le point d'entrée de notre application afin de mettre en place le routing :

// Fichier : ./src/index.js [extrait]
//...
import { Router, Route, IndexRoute, browserHistory } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";

const store = createStore(allReducers, undefined, autoRehydrate());
// On crée un nouvel historique à partir de celui fourni par react-router. Ceci afin de synchroniser les évènements de navigation avec notre store
const history = syncHistoryWithStore(browserHistory, store);

persistStore(store);
ReactDOM.render(
  <Provider store={store}>
    {/\* On créé notre router en lui passant en paramètre notre historique modifié \*/}
    <Router history={history}>
      {/* On définit notre route \*/}
      <Route
        path="/"
        component={props => <AppContainer {...props} title="HappyDrink" />}
      ></Route>
    </Router>
  </Provider>,
  document.getElementById("root")
);

petit zoom sur cette partie de code :

// Fichier : ./src/index.js [extrait]
<Route
  path="/"
  component={props => <AppContainer {...props} title="HappyDrink" />}
></Route>;

Ici nous définissons notre première route. Notre AppContainer sera ainsi accessible à l'url /.

La notation : component={(props) => <AppContainer {...props} title="HappyDrink"/>} permet de passer des props supplémentaires. Sinon, nous aurions simplement écrit : component={AppContainer}

Il ne nous manque plus qu'à mettre à jour notre store :

// Fichier : ./src/reducers/index.js [extrait]
//...
import { routerReducer } from "react-router-redux";

const allReducers = combineReducers({
  app: appReducer,
  establishments: establishmentReducer,
  routing: routerReducer
});

export default allReducers;

Avec ceci, nous aurons accès au state de notre routing.

Route imbriqué

Bon, c'est bien tout cela, mais nous allons utiliser notre header tout le long de l'application, et on ne va quand même pas faire du copier-coller ? si ?:

header

Alors, non en effet. Pour cela, nous allons imbriquer nos routes comme cela :

// Fichier : ./src/index.js [extrait]
//...
import AppContainer from "./containers/appContainer";
import HomeContainer from "./containers/homeContainer";
//...
ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route
        path="/"
        component={props => <HomeContainer {...props} title="HappyDrink" />}
      >
        <IndexRoute component={AppContainer} />
        <Route
          path="/example"
          component={
            UnComponentQuiNexistePasCarIlSertDExampleDeRouteSupplementaire
          }
        />
      </Route>
    </Router>
  </Provider>,
  document.getElementById("root")
);

Nous définissons une route d'url / lié au container HomeContainer qui sera notre "template" ( on construira tout de suite après le HomeContainer et son component).
Ensuite, à l'intérieur de celle-ci, nous définissons les routes "enfants" (IndexRoute permet de définir la route par défaut).

Ainsi, pour chacunes des urls "enfants", ce sera le component parent qui sera appelé. Pour afficher le contenu du component "enfant" lié à l'url appelée (c'est quand même ce que l'on veut faire), il sera nécessaire d'utiliser la variable this.props.children dans le component parent.

Voyons tout de suite cela en application.

Container et component Home

// Fichier : ./src/containers/homeContainer.js [nouveau fichier]
import { connect } from "react-redux";
import Home from "../components/home";

const mapStateToProps = state => {
  return {
    state: {
      app: state.app
    }
  };
};

const mapDispatchToProps = dispatch => {
  return {};
};

const HomeContainer = connect(mapStateToProps, mapDispatchToProps)(Home);

export default HomeContainer;
// Fichier : ./src/components/home.js [nouveau fichier]

import React, { Component } from "react";
import logo from "../assets/logo.svg";
import "../css/App.css";

class Home extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>
            Welcome "{this.props.state.app.pseudo}" to {this.props.title}
          </h2>
        </div>
        {/* On affiche l'enfant courant \*/}
        {this.props.children}
      </div>
    );
  }
}

export default Home;

Mise à jour du component App

Maintenant que home prend en charge le header, on peut enlever cette partie du component App :

// Fichier : ./src/components/App.js [extrait]
// On enlève l'import du logo qui ne sert plus à rien ici
// import logo from '../assets/logo.svg'
//...
render() {
  //...
  return (
    <div className="App-intro">
      <p>
        {" "}
        <a onClick={this.props.randomPseudo}>Changer le pseudo !</a>{" "}
      </p>
      <div>
        <input
          type="text"
          placeholder="search"
          value={this.props.state.app.textFilter}
          onChange={this.handleChange}
        />
      </div>
      <section>{listEstablishment}</section>
      <section>{this.props.state.app.dataFromAPI}</section>
    </div>
  );
};
//...

Et voilà ! On se retrouve avec le même affichage que la leçon précédente, mais maintenant notre application est parée pour avoir plusieurs pages :) !

4. Les happy-hours

Première étape : mettre à jour nos fixtures :

// Fichier : ./src/components/establishments/fixtures.js
export const establishments = [
  {
    id: "0890786GH",
    name: "Tonton",
    description: "Un super bar étudiant",
    happyhour: {
      text: "Des verres gratuits jusqu'au bout de la nuit",
      time: "20H00 - 02H00"
    }
  },
  {
    id: "0890786GD",
    name: "The Londow Town",
    description: "Un super bar à bière",
    happyhour: {
      text: "Une bierre achetée, une bière offerte",
      time: "21H30 - 23H00"
    }
  },
  {
    id: "MJLMH0389",
    name: "Australian Bar",
    description: "Un super bar dansant",
    happyhour: {
      text: "10 shots pour le prix d'un",
      time: "22H00 - 22H30"
    }
  }
];

ainsi que notre reducer establishmentReducer :

// Fichier : ./src/reducers/establishmentReducer.js [extraint]
//...
establishments.map(establishment => {
  initialState.push({
    id: establishment.id,
    name: establishment.name,
    description: establishment.description,
    happyhour: establishment.happyhour,
    isLiked: false,
    isDisliked: false,
    likeCounter: 0,
    dislikeCounter: 0,
    favori: false,
    visible: true
  });
  return establishment;
});
//...

Vu que nous avons modifié le state géré par Redux, je vous invite à "purger" votre store :

// Fichier : ./src/index.js [extrait]
persistStore(store).purge(); // Rechargez une fois la page avec `.purge()`, afin de mettre à jour le store stocké, puis vous pouvez l'enlever

Maintenant que nos données sont prêtes, ajoutons la nouvelle route :

// Fichier : ./src/index.js [extrait]
//...
import HappyhourContainer from "./containers/happyhourContainer"; // On va le créer par la suite
//...
ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route
        path="/"
        component={props => <HomeContainer {...props} title="HappyDrink" />}
      >
        <IndexRoute component={AppContainer} />
        {/* :id est un paramètre  \*/}
        <Route path="/happyhour/:id" component={HappyhourContainer} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById("root")
);

Ici, nous indiquons le paramètre id dans l'url de notre nouvelle route. Sachez que tout ce qui commence par : dans le path est un paramètre, cela nous permet de customiser notre url. Ce paramètre pourra être récupéré par la prop: this.props.params[name] ( Dans notre cas : this.props.params.id )

Afin de pouvoir accéder à cette nouvelle route, nous allons utiliser l'élément Link fournit par react-router :

// Fichier : ./src/components/establishments/Establishment.js [extrait]
import React, { Component } from "react";
import { Link } from "react-router";

class Establishment extends Component {
  render() {
    let id = this.props.establishment.id;
    let url = "/happyhour/" + id;
    //...
    return (
      <div className="establishment">
        {/*...*/}
        <div className="establishment-description">
          {/*...*/}
          <div>
            <Link to={url}>Voir l happy-hour </Link>
          </div>
        </div>

        {/*...*/}
      </div>
    );
  }
}

export default Establishment;

Vous remarquerez, que le paramètre id de notre route sera égal à l' id de l'établissement. Cela va nous permettre de récupérer par la suite les données dont nous aurons besoin.

Créons maintenant le container et le component qui se chargeront d'afficher l'happyhour de l'établissement :

// Fichier : ./src/containers/happyhourContainer.js
import { connect } from "react-redux";
import Happyhour from "../components/establishments/Happyhour";

const mapStateToProps = state => {
  return {
    state: {
      establishments: state.establishments
    }
  };
};
const mapDispatchToProps = dispatch => {
  return {};
};
const HappyhourContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Happyhour);

export default HappyhourContainer;
// Fichier : ./src/components/establishments/Happyhour.js
import React, { Component } from "react";

class Happyhour extends Component {
  goBack = () => {
    // Cela nous permet de renvoyer l'utilisateur à la dernière page visitée
    this.props.router.goBack();
  };

  render() {
    let establishment = this.props.state.establishments.find(
      establishment => establishment.id == this.props.params.id
    );

    return (
      <div className="happyhour">
        <button onClick={this.goBack}> Back </button>
        <h1> Happyhour chez : {establishment.name} </h1>
        <p> Horaire : {establishment.happyhour.time} </p>
        <p> {establishment.happyhour.text}</p>
      </div>
    );
  }
}

export default Happyhour;

Dans ce component, nous récupérons l'établissement selon l'id passé en paramètre de l'url et nous affichons les informations voulues.

Et voilà une bonne chose de faite !

3. Récapitulatif

Organisation du projet

src
|**_actions
| |
| |_**actionsTypes.js
| |
| |**_appActions.js
| |
| |_**establishmentActions.js
|
|**_assets
| |
| |_**logo.svg
|
|**_components
| |
| |_**establishments
| | |
| | |**_establishments.js
| | |
| | |_**fixtures.js
| | |
| | |**_Happyhour.js
| |
| |_**App.js
| |
| |**_home.js
|
|_**containers
| |
| |**_appContainer.js
| |
| |_**establishmentContainer.js
| |
| |\_**_HappyHourContainer.js
| |
| |_**homeContainer.js
|
|**_css
| |
| |_**App.css
| |
| |**_index.css
|
|_**reducers
| |
| |**_appReducer.js
| |
| |_**establishmentReducer.js
| |
| |**_index.js
|
|_**App.test.js
|
|\_\_\_index.js

Regardons un peu l'avancement de notre projet HappyDrink :

  • lister les bars.
  • filtrer la liste.
  • mettre en favori un bar.
  • visualiser l'happy-hour de celui-ci.
  • liker/disliker ce bar.

On a fini notre application !!!

Vous pourrez retrouver les sources de cette leçon à l'adresse suivante : github lecon_6

4. Pour la suite

Dans la prochaine et dernière leçon, nous verrons quelques éléments supplémentaires sur React et nous peaufinerons notre application (récupérer les données d'une API par exemple).

À propos de l'auteur - gkueny @Occitech

Développeur depuis maintenant 2 ans et demi, j'ai une grande affinité avec le front-end et les tests bien fait. Pas full-stack mais touche à tout, je suis également à l'aise sur du Symfony / php.