web-dev-qa-db-fra.com

simplifier le redux avec action générique et réducteur

Dans le projet React-Redux, les utilisateurs créent généralement actions multiples et réducteurs pour chaque composant connecté. Cependant, cela crée beaucoup de code pour de simples mises à jour de données. 

Est-ce une bonne pratique d’utiliser un seul action générique & réducteur pour encapsuler toutes les modifications de données, afin de (simplifier et accélérer le développement des applications}. 

Quels seraient les inconvénients ou la perte de performance en utilisant cette méthode. Parce que je ne vois pas de compromis important, et que cela facilite beaucoup le développement, et que nous pouvons tous les mettre dans un seul fichier! Exemple d'une telle architecture:

// Say we're in user.js, User page

// state
var initialState = {};

// generic action --> we only need to write ONE DISPATCHER
function setState(obj){
    Store.dispatch({ type: 'SET_USER', data: obj });
}

// generic reducer --> we only need to write ONE ACTION REDUCER
function userReducer = function(state = initialState, action){
    switch (action.type) {
        case 'SET_USER': return { ...state, ...action.data };
        default: return state;
    }
};

// define component
var User = React.createClass({
    render: function(){
        // Here's the magic...
        // We can just call the generic setState() to update any data.
        // No need to create separate dispatchers and reducers, 
        // thus greatly simplifying and fasten app development.
        return [
            <div onClick={() => setState({ someField: 1 })}/>,
            <div onClick={() => setState({ someOtherField: 2, randomField: 3 })}/>,
            <div onClick={() => setState({ orJustAnything: [1,2,3] })}/>
        ]
    }
});

// register component for data update
function mapStateToProps(state){
    return { ...state.user };
}

export default connect(mapStateToProps)(User);

Modifier

L’architecture typique de Redux suggère donc de créer:

  1. Fichiers centralisés avec toutes les actions
  2. Fichiers centralisés avec tous les réducteurs

La question est la suivante: pourquoi un processus en deux étapes? Voici une autre suggestion architecturale:

Créez (1 jeu de fichiers) contenant toutes les setXField() qui gèrent toutes les modifications de données. Et d'autres composants les utilisent simplement pour déclencher des changements. Facile. Exemple:

/** UserAPI.js
  * Containing all methods for User.
  * Other components can just call them.
  */

// state
var initialState = {};

// generic action
function setState(obj){
    Store.dispatch({ type: 'SET_USER', data: obj });
}

// generic reducer 
function userReducer = function(state = initialState, action){
    switch (action.type) {
        case 'SET_USER': return { ...state, ...action.data };
        default: return state;
    }
};


// API that we export
let UserAPI = {};

// set user name
UserAPI.setName = function(name){
    $.post('/user/name', { name }, function({ ajaxSuccess }){
        if (ajaxSuccess) setState({ name });
    });
};

// set user picture URL
UserAPI.setPicture = function(url){
    $.post('/user/picture', { url }, function({ ajaxSuccess }){
        if (ajaxSuccess) setState({ url });
    });
};

// logout, clear user
UserAPI.logout = function(){
    $.post('/logout', {}, function(){
        setState(initialState);
    });
};

// Etc, you got the idea...
// Moreover, you can add a bunch of other User related methods, 
// like some helper methods unrelated to Redux, or Ajax getters. 
// Now you have everything related to User available in a single file! 
// It becomes much easier to read through and understand.

// Finally, you can export a single UserAPI object, so other 
// components only need to import it once. 
export default UserAPI

Veuillez lire les commentaires dans la section de code ci-dessus. 

Maintenant au lieu d'avoir un tas d'actions/dispatchers/réducteurs. Vous avez 1 fichier contenant tout le nécessaire pour le concept utilisateur}. Pourquoi est-ce une mauvaise pratique? IMO, cela rend la vie du programmeur beaucoup plus facile, et les autres programmeurs peuvent simplement lire le fichier de haut en bas pour comprendre la logique métier, ils n'ont pas besoin de passer d'une action à l'autre./fichiers réducteurs. Zut, même redux-thunk n'est pas nécessaire! Et vous pouvez même tester les fonctions une à une. Donc, testabilité n'est pas perdu.

11
Maria

Premièrement, au lieu d'appeler store.dispatch dans le créateur de votre action, il devrait renvoyer un objet (action), ce qui simplifie les tests et active le rendu du serveur .

const setState = (obj) => ({
  type: 'SET_USER', 
  data: obj
})

onClick={() => this.props.setState(...)}

// bind the action creator to the dispatcher
connect(mapStateToProps, { setState })(User)

Vous devez également utiliser classe ES6 au lieu de React.createClass.

De retour au sujet, un créateur d’action plus spécialisé serait quelque chose comme:

const setSomeField = value => ({
  type: 'SET_SOME_FIELD',
  value,
});
...
case 'SET_SOME_FIELD': 
  return { ...state, someField: action.value };

Avantages de cette approche par rapport à votre générique

1. Plus grande réutilisabilité

Si someField est défini à plusieurs endroits, il est préférable d'appeler setSomeField(someValue) que setState({ someField: someValue })}.

2. Testabilité supérieure

Vous pouvez facilement tester setSomeField pour vous assurer qu'il ne modifie correctement que l'état associé. 

Avec le générique setState, vous pouvez également tester setState({ someField: someValue })}, mais rien ne garantit directement que tout votre code l'appellera correctement.
Par exemple. Quelqu'un de votre équipe pourrait faire une faute de frappe et appeler plutôt setState({ someFeild: someValue })}.

Conclusion

Les inconvénients ne sont pas tout à fait significatifs. Il est donc tout à fait judicieux d’utiliser le créateur d’actions génériques pour réduire le nombre de créateurs d’actions spécialisées si vous pensez que cela vaut la peine d’échanger votre projet.

MODIFIER

En ce qui concerne votre suggestion de placer les réducteurs et les actions dans le même fichier: il est généralement préférable de les conserver dans des fichiers séparés pour modularité ; C'est un principe général qui n'est pas propre à React. 

Vous pouvez toutefois placer les fichiers de réduction et d'action associés dans le même dossier, ce qui peut être meilleur/pire en fonction des exigences de votre projet. Voir this et this pour un aperçu. 

Vous auriez également besoin d'exporter userReducer pour votre réducteur de racine, sauf si vous utilisez plusieurs magasins, qui est généralement non recommandé

5
riwu

J'ai commencé à écrire un package pour le rendre plus facile et plus générique. Aussi pour améliorer les performances. Il est encore à ses débuts (couverture de 38%). Voici un petit extrait (si vous pouvez utiliser les nouvelles fonctionnalités de l’ES6), mais il existe également des alternatives.

import { create_store } from 'redux';
import { create_reducer, redup } from 'redux-decorator';

class State {
    @redup("Todos", "AddTodo", [])
    addTodo(state, action) {
        return [...state, { id: 2 }];
    }
    @redup("Todos", "RemoveTodo", [])
    removeTodo(state, action) {
        console.log("running remove todo");
        const copy = [...state];
        copy.splice(action.index, 1);
        return copy;
    }
}
const store = createStore(create_reducer(new State()));

Vous pouvez aussi même imbriquer votre état:

class Note{
        @redup("Notes","AddNote",[])
        addNote(state,action){
            //Code to add a note
        }
    }
    class State{
        aConstant = 1
        @redup("Todos","AddTodo",[])
        addTodo(state,action){
            //Code to add a todo
        }
        note = new Note();
    }
    // create store...
    //Adds a note
    store.dispatch({
        type:'AddNote'
    })
    //Log notes
    console.log(store.getState().note.Notes)

Beaucoup de documentation disponible sur le NGP. Comme toujours, n'hésitez pas à contribuer!

1
Eladian

J'utilise principalement redux pour mettre en cache les réponses de l'API, voici quelques cas où je pensais que c'était limité.

1) Que se passe-t-il si j'appelle des API différentes qui ont la même clé mais qui vont à un objet différent?

2) Comment puis-je prendre soin si les données sont un flux provenant d'un socket? Dois-je itérer l'objet pour obtenir le type (car le type sera dans l'en-tête et la réponse dans la charge utile) ou demander à ma ressource principale de l'envoyer avec un certain schéma.

3) Cela échoue également pour les API si nous utilisons un fournisseur tiers pour lequel nous n'avons aucun contrôle sur les résultats obtenus.

Il est toujours bon de contrôler le contenu des données, où Dans les applications très volumineuses, comme une application de surveillance du réseau nous risquons de remplacer les données si nous avons la même clé et le code JavaScript lâché peut mettre fin à cette situation de façon très étrange cela ne fonctionne que dans quelques cas où nous avons un contrôle total sur les données, ce qui est très peu similaire à cette application. 

1
karthik

Ok, je vais juste écrire ma propre réponse:

  • lorsque vous utilisez redux, posez-vous ces deux questions:

    1. Ai-je besoin d'accéder aux données de plusieurs composants?
    2. Ces composants sont-ils sur une autre arborescence? Ce que je veux dire, c'est que ce n'est pas un composant enfant.

      Si votre réponse est oui, utilisez redux pour ces données, car vous pouvez facilement transmettre ces données à vos composants via l'API connect(), ce qui les rend containers.

  • Parfois, si vous devez transférer des données à un composant parent, vous devez reconsidérer l'emplacement de votre état. Il y a une chose appelée Soulever l'État.

  • Si vos données ne concernent que votre composant, vous ne devez utiliser que setState pour ne pas perdre la portée. Exemple:

class MyComponent extends Component {
   constructor() {
       super()
       this.state={ name: 'anonymous' }
   }

   render() {
       const { name } = this.state
       return (<div>
           My name is { name }.
           <button onClick={()=>this.setState({ name: 'John Doe' })}>show name</button>
       </div>)
   }
}
  • Pensez également à maintenir un flux de données unidirectionnel. Ne connectez pas simplement un composant à redux store si les données sont déjà accessibles par son composant parent comme ceci:
<ChildComponent yourdata={yourdata} />
  • Si vous devez modifier l'état d'un parent à partir d'un enfant, transmettez simplement le contexte d'une fonction à la logique de votre composant enfant. Exemple:

En composant parent

updateName(name) {
    this.setState({ name })
}

render() {
    return(<div><ChildComponent onChange={::this.updateName} /></div>)
}

En composante enfant

<button onClick={()=>this.props.onChange('John Doe')}

Voici un bon article à ce sujet.

  • Juste pratique et tout va commencer à avoir un sens une fois que vous savez comment résumer correctement votre application pour séparer les préoccupations. Sur ces questions composition vs ihhertitance et penser en réaction est une très bonne lecture.
0
Lelouch

Une décision clé à prendre lors de la conception des programmes React/Redux est de savoir où placer la logique métier (il faut aller quelque part!).

Cela peut aller dans les composants React, dans les créateurs d'action, dans les réducteurs ou dans une combinaison de ceux-ci. Que la combinaison action générique/réducteur soit raisonnable ou non dépend de la direction que prend la logique applicative.

Si les composants React exécutent la majeure partie de la logique métier, les créateurs et les réducteurs d'action peuvent être très légers et peuvent être placés dans un fichier unique, comme vous le suggérez, sans aucun problème, sauf en rendant les composants React plus complexes.

La raison pour laquelle la plupart des projets React/Redux semblent avoir beaucoup de fichiers pour les créateurs d’action et les réducteurs, car une partie de la logique métier y est insérée, ce qui entraînerait un fichier très volumineux si la méthode générique était utilisée.

Personnellement, je préfère avoir des réducteurs très simples et des composants simples, et avoir un grand nombre d’actions pour faire abstraction de la complexité, comme demander des données d’un service Web aux créateurs d’actions, mais la "bonne" méthode dépend du projet à exécuter.

Une note rapide: Comme mentionné dans https://stackoverflow.com/a/50646935 , l'objet doit être renvoyé à partir de setState. En effet, il est possible que certains traitements asynchrones doivent avoir lieu avant l'appel de store.dispatch.

Un exemple de réduction du passe-partout est présenté ci-dessous. Ici, un réducteur générique est utilisé, ce qui réduit le code nécessaire, mais n’est possible que si la logique est gérée ailleurs pour que les actions soient aussi simples que possible.

import ActionType from "../actionsEnum.jsx";

const reducer = (state = {
    // Initial state ...
}, action) => {
    var actionsAllowed = Object.keys(ActionType).map(key => {
        return ActionType[key];
    });
    if (actionsAllowed.includes(action.type) && action.type !== ActionType.NOP) {
        return makeNewState(state, action.state);
    } else {
        return state;
    }
}

const makeNewState = (oldState, partialState) => {
    var newState = Object.assign({}, oldState);
    const values = Object.values(partialState);
    Object.keys(partialState).forEach((key, ind) => {
        newState[key] = values[ind];
    });
    return newState;
};

export default reducer;

tldr Il s'agit d'une décision de conception à prendre au tout début du développement, car elle affecte la manière dont une grande partie du programme est structurée.

0
A Jar of Clay

Performance sage pas beaucoup. Mais du point de vue de la conception, pas mal. En ayant plusieurs réducteurs, vous pouvez séparer les problèmes - chaque module ne concerne que lui-même. En ayant des créateurs d’action, vous ajoutez une couche d’indirection qui vous permet d’apporter des modifications plus facilement. En fin de compte, cela dépend toujours. Si vous n’avez pas besoin de ces fonctionnalités, une solution générique permet de réduire le code. 

0
miles_christian