Persister ses données

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

Aujourd'hui, on termine notre application et on regarde comment persister nos données.

Attention, cet article n'est pas à jour.

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

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. Proposition de correction

Dans la leçon précédente, nous avions laissé en suspens le filtrage de nos établissements. Il est temps de voir une façon de réaliser cette action.

Créons notre action

// Fichier : ./src/actions/actionsTypes.js [extrait]
//...
export const FILTER = "FILTER";
// Fichier : ./src/actions/appActions.js
//...
export function filter(text) {
  return {
    type: types.FILTER,
    data: {
      text: text
    }
  };
}

Modifions notre reducer

// Fichier : ./src/reducers/establishmentReducer.js [extrait]
//...
establishments.map(establishment => {
  initialState.push({
    id: establishment.id,
    name: establishment.name,
    description: establishment.description,
    isLiked: false,
    isDisliked: false,
    likeCounter: 0,
    dislikeCounter: 0,
    favori: false,
    visible: true // On ajoute la variable visible pour pouvoir filtrer
  });
  return establishment;
});

const establishment = (state = {}, action) => {
  switch (action.type) {
    case types.FILTER:
      // On compare la recherche au nom de notre établissement afin de savoir s'il doit être visible ou pas
      return {
        ...state,
        visible:
          state.name.toUpperCase().indexOf(action.data.text.toUpperCase()) >= 0
      };
    //...
  }
};

const establishmentsReducer = (state = initialState, action) => {
  switch (action.type) {
    case types.FILTER:
      return state.map(establishmentState =>
        establishment(establishmentState, action)
      );
    // ...
  }
};

export default establishmentsReducer;

Dispatchons notre action

Ajoutons une fonction dans notre appContainer, afin de dispatcher notre nouvelle action :

// Fichier : ./src/containers/appContainer.js [extrait]
//...
const mapDispatchToProps = dispatch => {
  return {
    //...
    filter: text => dispatch(appActions.filter(text))
  };
};
//...

Stocker le texte de la recherche :

Profitons de l'action filter qui est dispatchée pour mettre à jour la variable textFilter dans notre reducer app :

// Fichier : ./src/reducers/appReducer.js [extrait]
import * as types from "../actions/actionsTypes";

const initialState = {
  dataFromAPI: "",
  pseudo: "Inconnu",
  textFilter: "" // on ajoute notre variable
};

const appReducer = (state = initialState, action) => {
  switch (action.type) {
    //...
    // Et on la met à jour
    case types.FILTER:
      return {
        ...state,
        textFilter: action.data.text
      };
    default:
      return state;
  }
};
export default appReducer;

Maintenant que tout est en place, il ne nous manque plus qu'à mettre à jour notre component:

// Fichier : ./src/components/App.js [extrait]
//...
class App extends Component {
  //...
  // Fonction appelée à chaque changement de texte dans notre input
  handleChange = e => {
    this.props.filter(e.target.value);
  };

  render() {
    // On filtre
    const establishmentFilter = this.props.state.establishments.filter(
      e => e.visible
    );
    const listEstablishment = establishmentFilter.map(establishment => {
      return (
        <EstablishmentContainer
          key={establishment.id}
          establishment={establishment}
        />
      );
    });
    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>
     {/*...*/}
    );
  }
}

export default App;

3. Sauvegarder nos données

Pour l'instant, à chaque fois que nous rafraîchissons notre application, on perd tout notre state. Pas super-pratique tout cela.

On va donc voir une façon de persister nos données.

Pour cela, je vous propose d'utiliser le package npm redux-persist.

$ npm install --save redux-persist

Implémentation

Afin d'utiliser redux-persist, nous allons quelque peu modifier notre fichier d'entrée :

//...
// Fichier ./src/index.js [extrait]
import { persistStore, autoRehydrate } from "redux-persist";

const store = createStore(allReducers, undefined, autoRehydrate());
persistStore(store);
//persistStore(store).purge() : si vous voulez "purger" ce que vous avez enregistré
//...

Et c'est tout.

Redux-persist s'occupe de persister le store à chaque modification grâce à la fonction persistStore.

La fonction autoRehydrate s'occupe quant à elle de mettre à jour le store avec la dernière sauvegarde.

Mmmmmh, ça marche oui. Mais ce n'est pas super de garder le filtre au rechargement de la page non ?

Tout dépend de ce que l'on veut, mais il est tout à fait possible de ne pas sauvegarder cette partie. ( comment ça, je me parle à moi-même ?)

Nous avons plusieurs possibilités :

blacklister votre reducer

La première possibilité est de blacklister votre reducer comme cela :

//...
// Fichier ./src/index.js [extrait]
persistStore(store, { blacklist: ["app"] }); // on indique le nom de la clé du reducer à blacklister
//...

Cela aura pour effet de ne pas sauvegarder votre reducer et donc le state qui va avec.

Heuuu oui, mais moi c'est juste le filtre que je ne veux pas sauvegarder ..

Cela est également possible.

Ne mettre à jour qu'une partie du state de notre reducer

Pour cela, il vous faut prendre en charge le type de l'action REHYDRATE définit par redux-persist dans notre reducer.

// Fichier : ./src/reducers/appReducer.js [extrait]
import { REHYDRATE } from "redux-persist/constants";
//...
const appReducer = (state = initialState, action) => {
  switch (action.type) {
    //...
    case REHYDRATE:
      var incoming = action.payload.app;
      if (incoming)
        return {
          ...state,
          ...incoming,
          textFilter: ""
        };
      return state;
    //...
  }
};

export default appReducer;

Ici, nous indiquons à notre reducer de mettre à jour tout notre state avec ce que nous donne redux-persist, à l'exception du textFilter qui est remis à vide.

On n'oublie pas de modifier également notre establishmentsReducer afin de remettre à zéro la variable visible :

// Fichier : ./src/reducers/establishmensReducer.js [extrait]
import { REHYDRATE } from "redux-persist/constants";
//...
const establishment = (state = {}, action) => {
  switch (action.type) {
    case REHYDRATE:
      return {
        ...state,
        visible: true
      };
    // ...
  }
};

const establishmentsReducer = (state = initialState, action) => {
  switch (action.type) {
    case REHYDRATE:
      if (action.payload.establishments)
        return action.payload.establishments.map(establishmentState =>
          establishment(establishmentState, action)
        );
      return state;
    // ...
  }
};

export default establishmentsReducer;

Et enfin on met à jour notre fonction persistStore :

// Fichier ./src/index.js [extrait]
//...
persistStore(store);
//...

Utiliser le state "local"

Même si nous utilisons Redux, rien ne nous empêche d'utiliser le state "local" de nos components. J'entends par state "local" le state définit par la variable this.state et modifiable par la fonction setState.

En procédant ainsi, ce state ne sera pas sauvegardé. Cependant, vous perdrez les avantages liés à Redux dont nous avions parlé dans le précédent article.

Personnellement j'utilise Redux pour :

  • le state auquel je veux avoir accès à plusieurs endroits de mon application
  • le state dont je souhaite sauvegarder le contenu

Pour les autres, j'utilise le state "local".

4. Récapitulatif

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

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.

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

5. Pour la suite

Dans la prochaine leçon, nous verrons le routing de notre application afin de visualiser l'happy-hour de nos établissements.

gkueny

À propos de l'auteur - gkueny @ZEBet

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