web-dev-qa-db-fra.com

Réducteurs génériques/Actions dans React/Redux

J'essaie de déterminer comment extraire plusieurs données à utiliser dans le même composant. 

Tous les exemples que je vois avec React/Redux demandent des données très spécifiques et comportent des réducteurs et des actions permettant de gérer ce type de données. Cependant, je n'ai pas pu trouver d'informations sur le traitement de données plus génériques. 

Par exemple, j'ai plusieurs composants (ou catégories) différents sur mon site. Un de ces composants est Cards. Ainsi, si un utilisateur clique sur le lien pour /cards/hockey, il doit demander les données de hockey à l’API (s’il ne figure pas déjà dans le magasin) et les afficher dans la page Cartes. Si un utilisateur clique sur le lien pour /cards/football, il doit suivre la même procédure, en vérifiant s'il a déjà les données en magasin, s'il ne les extrait pas de l'API et en affichant la page Cartes avec ces données. 

Un autre type de composant pourrait être stats avec des statistiques sur différentes équipes sportives.

Je ne saurai pas toujours quels types de cartes sont disponibles à l'avance, je ne peux donc pas coder en dur les types de sports spécifiques dans mon application. 

Donc, dans ce cas, j'aimerais créer uniquement deux composants: les cartes et les statistiques, mais disposer de données chargées de manière dynamique pour remplir ces composants.

À l'heure actuelle, j'ai trop de répétitions et c'est codé en dur. Cela signifie que je ne peux pas ajouter de nouveaux types de manière dynamique à l'avenir sans créer un nouveau code pour gérer chacun de ces types.

Ainsi, par exemple, à l’heure actuelle, j’ai /actions/footballCardActions.js et /actions/hockeyCardActions.js. J'ai ensuite /reducers/footballCardReducers.js et /reducers/hockeyCardReducers.js. Je pourrais aussi avoir des composants similaires pour le composant Stats.

Je spécifie également des statuts tels que FETCH_HOCKEY_CARDS_SUCCESS ou FETCH_FOOTBALL_CARDS_SUCCESS.

Là encore, ils sont tous codés en dur, ce qui rend l’évolutivité difficile.

Un exemple que j'essaie de suivre est https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks - mais là encore, il utilise des requêtes de données très spécifiques, plutôt que génériques.

Que puis-je faire pour que mon code fonctionne de manière plus générique, de sorte que je n'ai pas besoin de coder en dur des ensembles de données spécifiques. Existe-t-il de bons tutoriels traitant d'une situation similaire?

Plus de précision

Un de mes composants (écrans) est un écran de carte de sport. Le système de menus (avec les liens) est automatiquement généré sur le site par une API, donc je ne sais pas toujours quels liens sont disponibles. Donc, il peut y avoir des liens pour le hockey, le football, ainsi que pour un certain nombre d'autres sports auxquels je n'ai pas pensé. Lorsque le lien de menu est cliqué, il appelle l'API pour ce type de sport et affiche les données sur l'écran de la carte sportive. 

Sur la base du lien ci-dessus (et d'autres sites similaires), j'ai découvert comment coder en dur chaque demande relative à un sport spécifique dans la section Actions et Réducteurs, mais je n'ai pas été en mesure de le ne connais pas les sports à l'avance.

Précisions supplémentaires sur la base des réponses actuelles

Si quelqu'un ajoute un nouveau sport à la base de données API appelée MuffiBall, mon application doit pouvoir le gérer. On ne peut donc pas s’attendre à ajouter un nouveau code JavaScript pour chaque nouveau sport ajouté à l’API. 

Toutes les cartes sportives extraites de la base de données suivent la même structure.

Un aperçu de mon code actuel

index.js

//index.js
//Other imports here (not shown)
import Cards from './components/CardsPage'
import * as cardActions from './actions/cardActions';
import * as statsActions from './actions/statsActions';

import configureStore from './store/configureStore';

const store = configureStore();

/* Bad place to put these, and currently I am expected to know what every sport is*/
store.dispatch(hockeyActions.fetchHockey());
store.dispatch(footballActions.fetchFootball());
store.dispatch(muffiballActions.fetchMuffiball());


render(
  <Provider store={store}>
          <Router>
                <div>

                    /* Navigation menu here (not shown) */
                    /* Currently it is manually coded, */
                    /* but I will be automatically generating it based on API */

                      <Route exact path="/" component={Home} />
                      <Route path="/about" component={About} />
                      <Route path="/cards/:val" component={Cards} />
                      <Route path="/stats/:val" component={Stats} />
                </div>
          </Router>
  </Provider>,
  document.getElementById('app')
);

store/configureStore.js

// store/configureStore.js
import {createStore, compose, applyMiddleware} from 'redux';
// Import thunk middleware
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

export default function configureStore(initialState) {
  return createStore(rootReducer, initialState,
    // Apply to store
    applyMiddleware(thunk)
  );
}

actions/actionTypes

// actions/actionTypes

export const FETCH_HOCKEY_SUCCESS = 'FETCH_HOCKEY_SUCCESS';
export const FETCH_FOOTBALL_SUCCESS = 'FETCH_FOOTBALL_SUCCESS';
export const FETCH_MUFFIBALL_SUCCESS = 'FETCH_MUFFIBALL_SUCCESS';

actions/hockeyActions.js (un fichier de ce type pour chaque sport - nécessité de créer ce fichier générique):

// hockeyActions.js (one such file for every sport - need to make this one generic file):

import Axios from 'axios';

const apiUrl = '/api/hockey/';
// Sync Action
export const fetchHockeySuccess = (hockey) => {
  return {
    type: 'FETCH_HOCKEY_SUCCESS',
    hockey
  }
};


//Async Action
export const fetchHockey = () => {
  // Returns a dispatcher function
  // that dispatches an action at a later time
  return (dispatch) => {
    // Returns a promise
    return Axios.get(apiUrl)
      .then(response => {
        // Dispatch another action
        // to consume data

        dispatch(fetchHockeySuccess(response.data))
      })
      .catch(error => {
        console.log(error)
        throw(error);
      });
  };
};

réducteurs/hockeyReducers.js (un fichier de ce type pour chaque sport - nécessité de créer ce fichier générique)

// reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)

import * as actionTypes from '../actions/actionTypes'

export const hockeyReducer = (state = [], action) => {
  switch (action.type) {
    case actionTypes.FETCH_HOCKEY_SUCCESS:
          return action.hockey;
    default:
          return state;
  }
};

réducteurs/index.js

// reducers/index.js

import { combineReducers } from 'redux';
import {hockeyReducer} from './hockeyReducers'
import {footballReducer} from './footballReducers'
import {muffiballReducer} from './muffiballReducers'

export default combineReducers({
  hockey: hockeyReducer,
  football: footballReducer,
  muffiball: muffiballReducer,
  // More reducers for each sport here
});

composants/CardsPage.js:

//components/CardsPage.js

import React from 'react';
import { connect } from 'react-redux';

class Cards extends React.Component{
  constructor(props){
    super(props);

    this.state = {
        data: this.props.data,
    }

  }

  componentWillReceiveProps(nextProps){
        this.setState({
                data: nextProps.data,
        })
  }

  render(){

    return(
        {/* cards displayed from this.state.data */}
    )
  }
}

const mapStateToProps = (state, ownProps) => {
  return {
    data: state[ownProps.match.params.val]
  }
};

export default connect(mapStateToProps)(Cards);
7
kojow7

prenez du recul et identifiez les types de données ayant des formes uniques, par exemple cards et stats. Vous construirez une tranche de magasin pour chacun d'eux avec ses propres actions, réducteurs et sélecteurs. Le sport ne devrait être qu'une variable que vous utiliseriez comme argument pour vos actions et vos sélecteurs.

Action Async

export const fetchCards = (sport) => {
  return (dispatch) => {
    return Axios.get(`/api/${sport}/`)
      .then(response =>
        dispatch(fetchCardSuccess({ sport, data: response.data }))
      )
      .catch(error => {
        console.log(error)
        throw(error);
      });
  };
};

Réducteur

export const cardReducer = (state = {}, action) => {
  switch (action.type) {
    case actionTypes.FETCH_CARD_SUCCESS:
      return { ...state, [action.sport]: action.data };
    default:
      return state;
  }
};

Sélecteur de cartes

export const getSport(state, sport) {
  return state.cards[sport];
}

Vous voudrez probablement une autre tranche pour gérer une liste des sports disponibles, extraits du serveur, et d'autres données globales.

5
lecstor

Donc, cela suppose que vos "données génériques" auront toujours la même forme.

Vous pourriez avoir un composant <Results /> générique. Vous ne savez pas comment vous faites le routage, mais vous pouvez utiliser le nom de chemin de l'URL pour déterminer les données à récupérer et à afficher.

Le composant de route (React Router 4) pourrait ressembler à ceci:

<Route path="/cards/:id" render={props => <Results {...props} />}

Ensuite, dans votre composant <Results/>, vous pouvez utiliser react-redux pour mapper votre état redux aux accessoires du composant. Dans componentDidMount, vous pouvez voir si vous avez les données appropriées. Si vous ne disposez pas des données appropriées, envoyez une action de composantDidMount pour la récupérer. Quelque chose comme ça

import { connect } from 'react-redux';
import React from 'react';
import { fetchDataAction } from './actions';

class Results extends React.Component {
  componentDidMount() {
    // check if results exists, if not then fire off an action to get 
    // data. Use whatever async redux pattern you want
    if (!this.props.results) {
      this.props.fetchData();
    }
  }

  render() { /* DO SOMETHING WITH RESULTS, OR LACK OF */ }
}

const mapStateToProps = (state, ownProps) => ({
  results: state.results[ownProps.match.params.id],
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData() {
    // send path parameter via action to kick off async fetch
    dispatch(fetchDataAction(ownProps.match.params.id));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(Results);

Vous pourriez avoir un réducteur de résultats qui serait simplement un objet qui mappera la catégorie aux résultats. Voici à quoi pourrait ressembler le réducteur de résultats:

export default (state = {}, action) => {
  switch(action.type) {
    case 'FETCH_LOADED':
      const { payload: { type, results } } = action;
      return {
        ...state,
        [type]: results,
      };
    default:
      return state;
  };
};
4
pizza-r0b

Une méthodologie qui gagne en popularité pour les actions/réducteurs redux réutilisables est Redux Ducks . Voici une bonne bibliothèque d'aide et exemple à implémenter dans votre base de code.

Construire à partir de l'exemple dans le lien ci-dessus qui ressemblerait à ceci:

// remoteObjDuck.js

import Duck from 'extensible-duck'
import axios from 'axios'

export default function createDuck({ namespace, store, path, initialState={} }) {
  return new Duck({
    namespace, store,

    consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },

    types: [
      'UPDATE',
      'FETCH', 'FETCH_PENDING',  'FETCH_FULFILLED',
      'POST',  'POST_PENDING',   'POST_FULFILLED',
    ],

    reducer: (state, action, { types, statuses, initialState }) => {
      switch(action.type) {
        case types.UPDATE:
          return { ...state, obj: { ...state.obj, ...action.payload } }
        case types.FETCH_PENDING:
          return { ...state, status: statuses.LOADING }
        case types.FETCH_FULFILLED:
          return { ...state, obj: action.payload.data, status: statuses.READY }
        case types.POST_PENDING:
        case types.PATCH_PENDING:
          return { ...state, status: statuses.SAVING }
        case types.POST_FULFILLED:
        case types.PATCH_FULFILLED:
          return { ...state, status: statuses.SAVED }
        default:
          return state
      }
    },

    creators: ({ types }) => ({
      update: (fields) => ({ type: types.UPDATE, payload: fields }),
      get:        (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
      post:         () => ({ type: types.POST, payload: axios.post(path, obj) }),
      patch:        () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
    }),

    initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
  })
}

et chaque sport créerait un seul canard qui réutiliserait les mêmes fonctionnalités.

Le hockey:

// hockeyDuck.js

import createDuck from './remoteObjDuck'

export default createDuck({ namespace: 'my-app', store: 'hockeyCards', path: '/cards/hockey' })

Football:

// footballDuck.js

    import createDuck from './remoteObjDuck'

    export default createDuck({ namespace: 'my-app', store: 'footballCards', path: '/cards/football' })

Puis combinez les réducteurs dans le magasin:

// réducteurs.js

import { combineReducers } from 'redux'
import footballDuck from './footballDuck'
import hockeyDuck from './hockeyDuck'

export default combineReducers({ [footballDuck.store]: footballDuck.reducer, [hockeyDuck.store]: hockeyDuck.reducer })

Si vous voulez ajouter dynamiquement des réducteurs à redux à la volée, vous devrez utiliser quelque chose comme: https://github.com/ioof-holdings/redux-dynamic-reducer . Ensuite, vous pouvez créer le canard à la volée en fonction de la réponse à votre appel API:

//get from API
var sport = "football";
var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
store.attachReducer({ [footballDuck.store]: footballDuck.reducer });
2
Cody S