wake-up-neo.com

vereinfachen Sie die Reduzierung mit generischen Aktionen und Reduzierungen

Im React-Redux-Projekt erstellen die Benutzer normalerweise mehrere Aktionen und Reduzierungen für jede verbundene Komponente. Dadurch wird jedoch sehr viel Code für einfache Datenaktualisierungen erstellt. 

Ist es eine gute Praxis, eine einzige generische Aktion & Reduzierer zu verwenden, um alle Datenänderungen zu kapseln, um die App-Entwicklung zu vereinfachen und zu beschleunigen

Was wären die Nachteile oder der Leistungsverlust bei dieser Methode. Weil ich keinen signifikanten Kompromiss sehe und es die Entwicklung wesentlich vereinfacht, und wir alle diese Dateien in eine einzige Datei speichern können! Beispiel einer solchen Architektur:

// 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);

Bearbeiten

So schlägt die typische Redux-Architektur Folgendes vor:

  1. Zentralisierte Dateien mit allen Aktionen
  2. Zentralisierte Dateien mit allen Reduzierungen

Die Frage ist, warum ein 2-Schritt-Prozess ? Hier ist ein weiterer architektonischer Vorschlag:

Erstellen Sie 1 Dateisatz , der alle setXField() enthält, die alle Datenänderungen behandeln. Und andere Komponenten verwenden sie einfach, um Änderungen auszulösen. Einfach. Beispiel:

/** 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

Bitte lesen Sie die Kommentare im obigen Codeabschnitt. 

Jetzt, anstatt eine Reihe von Aktionen/Dispatchern/Reduktionen zu haben. Sie haben 1 Datei, die alles enthält, was für das Benutzerkonzept benötigt wird . Warum ist es eine schlechte Praxis? IMO, macht das Leben des Programmierers viel einfacher , und andere Programmierer können die Datei einfach von oben nach unten durchlesen, um die Geschäftslogik zu verstehen, sie müssen nicht zwischen Aktionen/Reduzierungen hin und her wechseln Dateien. Heck, sogar redux-thunk wird nicht benötigt! Und Sie können die Funktionen auch einzeln testen. So Testbarkeit geht nicht verloren.

11
Maria

Anstatt store.dispatch in Ihrem Aktionsersteller aufzurufen, sollte stattdessen ein Objekt (action) zurückgegeben werden, was das Testen vereinfacht und das Rendern von Servern ermöglicht.

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

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

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

Sie sollten auch ES6 class anstelle von React.createClass verwenden.

Zurück zum Thema wäre ein spezialisierter Aktionsersteller so etwas wie:

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

Vorteile dieses Ansatzes gegenüber Ihrem generischen Ansatz

1. Höhere Wiederverwendbarkeit

Wenn someField an mehreren Stellen festgelegt ist, ist es einfacher, setSomeField(someValue) als setState({ someField: someValue })} aufzurufen.

2. Höhere Testbarkeit

Sie können leicht setSomeField testen, um sicherzustellen, dass nur der zugehörige Status korrekt geändert wird. 

Mit dem generischen setState können Sie auch auf setState({ someField: someValue })} testen, aber es gibt keine direkte Garantie dafür, dass der gesamte Code dies korrekt aufruft.
Z.B. Jemand in Ihrem Team könnte einen Tippfehler machen und stattdessen setState({ someFeild: someValue })} anrufen.

Fazit

Die Nachteile sind nicht gerade signifikant, daher ist es in Ordnung, den generischen Aktionsersteller zu verwenden, um die Anzahl der spezialisierten Aktionsersteller zu reduzieren, wenn Sie der Meinung sind, dass sich das für Ihr Projekt lohnt.

BEARBEITEN

In Bezug auf Ihren Vorschlag, Reduzierungen und Aktionen in derselben Datei abzulegen: Im Allgemeinen sollten diese für Modularität in separaten Dateien aufbewahrt werden. Dies ist ein allgemeiner Grundsatz, der nicht nur für React gilt. 

Sie können jedoch verwandte Reduzier- und Aktionsdateien im selben Ordner ablegen, was je nach Projektanforderungen besser oder schlechter sein kann. Siehe das und das für einen Hintergrund. 

Sie müssen auch userReducer für Ihr Root-Reduktionsprogramm exportieren, es sei denn, Sie verwenden mehrere Stores, was im Allgemeinen nicht empfohlen ist. 

5
riwu

Ich habe angefangen, ein package zu schreiben, um es einfacher und generischer zu gestalten. Auch zur Verbesserung der Leistung. Es befindet sich noch in einem frühen Stadium (38% Abdeckung). Hier ist ein kleiner Ausschnitt (wenn Sie neue ES6-Funktionen nutzen können), es gibt jedoch auch Alternativen.

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()));

Sie können sogar Ihren Staat verschachteln:

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)

Auf NPM sind viele Dokumentationen verfügbar. Fühlen Sie sich wie immer gerne dabei!

1
Eladian

Ich verwende meistens Redux, um API-Antworten meistens zwischenzuspeichern. Hier sind einige Fälle, in denen ich dachte, es sei begrenzt.

1) Was ist, wenn ich verschiedene APIs anrufe, die den gleichen KEY haben, aber zu einem anderen Objekt gehen?

2) Wie kann ich darauf achten, dass es sich bei den Daten um einen Stream von einem Socket handelt? Muss ich das Objekt iterieren, um den Typ zu erhalten (da der Typ im Header und in der Antwort in der Payload enthalten sein wird) oder bitten Sie meine Back-End-Ressource, diesen mit einem bestimmten Schema zu senden.

3) Dies schlägt auch für api fehl, wenn wir einen Drittanbieter verwenden, bei dem wir keine Kontrolle über die Ausgabe haben, die wir erhalten.

Es ist immer gut, die Kontrolle darüber zu haben, welche Daten wohin gehen. In Apps, die sehr groß sind, etwa in einer Netzwerküberwachungsanwendung , werden die Daten möglicherweise überschrieben, wenn derselbe TASTE verwendet wird und durch das Eintippen von JavaScript dies beendet wird auf viel seltsame Weise funktioniert dies nur für wenige Fälle, in denen wir die vollständige Kontrolle über die Daten haben, was sehr wenige dieser Anwendung ähneln. 

1
karthik

Okay, ich werde nur meine eigene Antwort schreiben:

  • wenn Sie Redux verwenden, stellen Sie sich diese beiden Fragen:

    1. Benötige ich Zugriff auf die Daten über mehrere Komponenten hinweg?
    2. Befinden sich diese Komponenten in einem anderen Knotenbaum? Ich meine, es ist keine untergeordnete Komponente.

      Lautet Ihre Antwort "Ja", verwenden Sie "Redux" für diese Daten, da Sie diese Daten einfach über die API "connect()" an Ihre Komponenten übergeben können, wodurch sie in "containers" umgewandelt werden.

  • Wenn Sie manchmal feststellen, dass Sie Daten an eine übergeordnete Komponente übergeben müssen, müssen Sie überdenken, wo Ihr Zustand lebt. Es gibt eine Sache namens Lifting the State Up.

  • Wenn Ihre Daten nur für Ihre Komponente von Bedeutung sind, sollten Sie setState nur verwenden, um Ihren Bereich eng zu halten. Beispiel:

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>)
   }
}
  • Denken Sie auch daran, den unidirektionalen Datenfluss aufrechtzuerhalten. Verbinden Sie eine Komponente nicht einfach mit dem Redux-Speicher, wenn die übergeordnete Komponente bereits auf die Daten zugreifen kann:
<ChildComponent yourdata={yourdata} />
  • Wenn Sie den Status eines übergeordneten Elements von einem untergeordneten Element ändern müssen, übergeben Sie einfach den Kontext einer Funktion an die Logik Ihrer untergeordneten Komponente. Beispiel:

In übergeordneter Komponente

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

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

In untergeordneter Komponente

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

Hier ist ein guter Artikel darüber.

  • Üben Sie einfach und alles wird Sinn machen, sobald Sie wissen, wie Sie Ihre App richtig abstrahieren, um Probleme zu trennen. In dieser Hinsicht sind Komposition vs Vererbung und Denken in Reaktion eine sehr gute Lektüre.
0
Lelouch

Beim Entwerfen von React/Redux-Programmen ist eine wichtige Entscheidung zu treffen, wo die Geschäftslogik abgelegt werden muss (sie muss irgendwo hingehen!).

Es könnte in den React-Komponenten, in den Aktionserstellern, in den Reduktionen oder in einer Kombination davon gehen. Ob die generische Kombination Aktion/Reduzierer sinnvoll ist, hängt davon ab, wo sich die Geschäftslogik bewegt.

Wenn die React-Komponenten den Großteil der Geschäftslogik beherrschen, können die Aktionsersteller und -reduzierer sehr leichtgewichtig sein und können, wie von Ihnen vorgeschlagen, problemlos in einer einzigen Datei abgelegt werden, es sei denn, die React-Komponenten werden komplexer.

Der Grund dafür, dass die meisten React/Redux-Projekte scheinbar viele Dateien für Aktionsersteller und -reduzierer enthalten, da einige der Geschäftslogiken dort eingefügt werden und daher zu einer sehr aufgeblähten Datei führen würden, wenn die generische Methode verwendet würde.

Ich persönlich bevorzuge sehr einfache Reduktionen und einfache Komponenten und habe eine große Anzahl von Aktionen, um die Komplexität wie das Anfordern von Daten von einem Webservice an die Ersteller der Action abzuräumen, aber der "richtige" Weg hängt von dem vorliegenden Projekt ab.

Ein kurzer Hinweis: Wie in https://stackoverflow.com/a/50646935 erwähnt, sollte das Objekt von setState zurückgegeben werden. Dies liegt daran, dass möglicherweise einige asynchrone Verarbeitung ausgeführt werden muss, bevor store.dispatch aufgerufen wird.

Ein Beispiel für die Reduzierung der Heizplatte ist unten. Hier wird ein generischer Reducer verwendet, der den erforderlichen Code reduziert, es ist jedoch nur möglich, dass die Logik an anderer Stelle gehandhabt wird, so dass Aktionen so einfach wie möglich gemacht werden.

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 Dies ist eine Entwurfsentscheidung, die zu einem frühen Zeitpunkt in der Entwicklung getroffen werden muss, da sie die Struktur eines großen Teils des Programms beeinflusst.

0
A Jar of Clay

Zunächst einige Terminologie :

  • Aktion: eine Nachricht, die wir Versand an alle Reduzierungen. Es kann alles sein. Normalerweise ist es ein einfaches Javascript-Objekt wie const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
  • Aktionstyp: Eine Konstante, die von den Aktionserstellern zum Erstellen einer Aktion und von den Reduzierern zum Verstehen der gerade empfangenen Aktion verwendet wird. Sie verwenden sie, um die Eingabe von 'SOME_ACTION' Sowohl in den Aktionserstellern als auch in den Reduzierern zu vermeiden. Sie definieren einen Aktionstyp wie const SOME_ACTION = 'SOME_ACTION', Damit Sie ihn in den Aktionserstellern und in den Reduzierern import können.
  • Aktionsersteller: Eine Funktion, die eine Aktion erstellt und an die Reduzierer verteilt.
  • Reducer: Eine Funktion, die all Aktionen empfängt, die an das Geschäft gesendet werden, und die für die Aktualisierung des Status für diesen Redux verantwortlich ist store (Sie haben möglicherweise mehrere Stores, wenn Ihre Anwendung komplex ist).

Nun zur Frage.

Ich denke, dass ein generischer Action-Creator keine großartige Idee ist.

Ihre Anwendung muss möglicherweise die folgenden Aktionsersteller verwenden:

fetchData()
fetchUser(id)
fetchCity(lat, lon)

Die Logik des Umgangs mit einer anderen Anzahl von Argumenten in einem einzigen Aktionsersteller zu implementieren, klingt für mich nicht richtig.

Ich denke, es ist viel besser, viele kleine Funktionen zu haben, weil sie unterschiedliche Verantwortlichkeiten haben. Beispielsweise sollte fetchUser nichts mit fetchCity zu tun haben.

Zunächst erstelle ich ein Modul für alle meine Aktionstypen und Aktionsersteller. Wenn meine Anwendung wächst, kann ich die Aktionsersteller in verschiedene Module aufteilen (z. B. actions/user.js, actions/cities.js), Aber ich halte separate Module für Aktionstypen für etwas übertrieben.


Was die Reduzierstücke betrifft, denke ich, dass ein einzelnes Reduzierstück eine gangbare Option ist, wenn Sie nicht mit zu vielen Aktionen fertig werden müssen.

Ein Reduzierer empfängt alle Aktionen, die von den Aktionserstellern ausgelöst wurden. Durch Betrachten von action.type Wird dann ein neuer Status des Speichers erstellt. Da Sie sich ohnehin um alle eingehenden Aktionen kümmern müssen, finde ich es schön, die gesamte Logik an einem Ort zu haben. Dies ist natürlich schwierig, wenn Ihre Anwendung wächst (z. B. ist ein switch/case Zur Verarbeitung von 20 verschiedenen Aktionen nicht sehr wartbar).

Sie können mit einem einzelnen Reduzierer beginnen, die Bewegung zu mehreren Reduzierern und diese in einem Wurzelreduzierer mit der Funktion combineReducer kombinieren.

0
jackdbd

Leistung weise nicht viel. Aus gestalterischer Sicht jedoch einige. Durch mehrere Reduzierungen können Sie die Bedenken voneinander trennen - jedes Modul ist nur mit sich selbst beschäftigt. Durch das Erstellen von Aktionserstellern fügen Sie eine Ebene der Indirektion hinzu, mit der Sie Änderungen leichter vornehmen können. Am Ende kommt es immer noch darauf an, wenn Sie diese Funktionen nicht benötigen, hilft eine generische Lösung, den Code zu reduzieren. 

0
miles_christian