Redux: Wie erstelle ich eine generische Liste als Reducer und Component Enhancer?

Erstellt am 30. Sept. 2015  ·  50Kommentare  ·  Quelle: reduxjs/redux

Hallo. Was wäre eine gute Möglichkeit, das Zählerbeispiel auf eine dynamische Liste unabhängiger Zähler zu erweitern?

Mit dynamisch meine ich, dass es in der Benutzeroberfläche am Ende der Zählerliste die Schaltflächen + und - geben würde, um einen neuen Zähler zur Liste hinzuzufügen oder den letzten zu entfernen.

Im Idealfall würden das Untersetzungsgetriebe und die Komponente so bleiben, wie sie sind. Wie würde man einen verallgemeinerten Listenspeicher und eine Komponente erstellen, um beliebige Entitäten zu sammeln? Wäre es möglich, die list store+Komponente noch weiter zu verallgemeinern, um sowohl Zähler als auch todo-Elemente aus dem todomvc-example zu nehmen?

Es wäre toll, so etwas in den Beispielen zu haben.

examples

Hilfreichster Kommentar

Wie würde man einen verallgemeinerten Listenspeicher und eine Komponente erstellen, um beliebige Entitäten zu sammeln? Wäre es möglich, die list store+Komponente noch weiter zu verallgemeinern, um sowohl Zähler als auch todo-Elemente aus dem todomvc-Beispiel zu nehmen?

Ja, durchaus möglich.
Sie möchten einen Reduzierer höherer Ordnung und eine Komponente höherer Ordnung erstellen.

Für Reduzierer höherer Ordnung siehe den Ansatz hier:

function list(reducer, actionTypes) {
  return function (state = [], action) {
    switch (action.type) {
    case actionTypes.add:
      return [...state, reducer(undefined, action)];
    case actionTypes.remove:
      return [...state.slice(0, action.index), ...state.slice(action.index + 1)];
    default:
      const { index, ...rest } = action;
      if (typeof index !== 'undefined') {
        return state.map(item => reducer(item, rest));
      }
      return state;
    }
  }
}

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return counter + 1;
  case 'DECREMENT':
    return counter - 1;
  }
}

const listOfCounters = list(counter, {
  add: 'ADD_COUNTER',
  remove: 'REMOVE_COUNTER'
});

const store = createStore(listOfCounters);
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'INCREMENT',
  index: 0
});
store.dispatch({
  type: 'INCREMENT',
  index: 1
});
store.dispatch({
  type: 'REMOVE_COUNTER',
  index: 0
});

(Ich habe es nicht ausgeführt, aber es sollte mit minimalen Änderungen funktionieren.)

Alle 50 Kommentare

Ich stimme zu, dass dies ein gutes Beispiel ist.
Redux ähnelt hier Elm Architecture , also lassen Sie sich dort von Beispielen inspirieren.

Wie würde man einen verallgemeinerten Listenspeicher und eine Komponente erstellen, um beliebige Entitäten zu sammeln? Wäre es möglich, die list store+Komponente noch weiter zu verallgemeinern, um sowohl Zähler als auch todo-Elemente aus dem todomvc-Beispiel zu nehmen?

Ja, durchaus möglich.
Sie möchten einen Reduzierer höherer Ordnung und eine Komponente höherer Ordnung erstellen.

Für Reduzierer höherer Ordnung siehe den Ansatz hier:

function list(reducer, actionTypes) {
  return function (state = [], action) {
    switch (action.type) {
    case actionTypes.add:
      return [...state, reducer(undefined, action)];
    case actionTypes.remove:
      return [...state.slice(0, action.index), ...state.slice(action.index + 1)];
    default:
      const { index, ...rest } = action;
      if (typeof index !== 'undefined') {
        return state.map(item => reducer(item, rest));
      }
      return state;
    }
  }
}

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return counter + 1;
  case 'DECREMENT':
    return counter - 1;
  }
}

const listOfCounters = list(counter, {
  add: 'ADD_COUNTER',
  remove: 'REMOVE_COUNTER'
});

const store = createStore(listOfCounters);
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'INCREMENT',
  index: 0
});
store.dispatch({
  type: 'INCREMENT',
  index: 1
});
store.dispatch({
  type: 'REMOVE_COUNTER',
  index: 0
});

(Ich habe es nicht ausgeführt, aber es sollte mit minimalen Änderungen funktionieren.)

Danke - ich werde versuchen, diesen Ansatz zum Laufen zu bringen.

Ich frage mich immer noch, ob ich die Listenfunktion über combineReducers wiederverwenden kann, um sowohl eine Liste mit Zählern als auch eine Liste mit Aufgaben zu haben. Und ob das sinnvoll wäre. Aber ich werde es auf jeden Fall versuchen.

Ich frage mich immer noch, ob ich die Listenfunktionalität durch CombineReducers wiederverwenden kann, um sowohl eine Liste von Zählern als auch eine Liste von Aufgaben zu haben. Und ob das sinnvoll wäre. Aber ich werde es auf jeden Fall versuchen.

Ja, ganz:

const reducer = combineReducers({
  counterList: list(counter, {
    add: 'ADD_COUNTER',
    remove: 'REMOVE_COUNTER'
  }),
  todoList: list(counter, {
    add: 'ADD_TODO',
    remove: 'REMOVE_TODO'
  }),
});

@gaearon wie soll die Listenaktion ihre index bekommen?

Wir haben es geschafft, mit Ihren Anweisungen einen Reduzierer höherer Ordnung zu erstellen, aber wir kämpfen mit der Komponente höherer Ordnung. Derzeit ist unsere Komponente nicht generisch genug, um mit anderen Komponenten als Counter verwendet zu werden. Unser Problem ist, wie man den Index auf generische Weise zu den Aktionen hinzufügt.

Sie können unsere Lösung hier sehen: https://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c

Ich habe dem Commit einen Kommentar hinzugefügt, um den problematischen Teil hervorzuheben.

Es wäre großartig, einen allgemeinen Listenreduzierer + eine Komponente zu haben, die Listen von Zählerlisten erstellen kann.

Derzeit ist unsere Komponente nicht generisch genug, um mit anderen Komponenten als Counter verwendet zu werden. Unser Problem ist, wie man den Index auf generische Weise zu den Aktionen hinzufügt.

Können Sie erklären, was Sie mit „Hinzufügen des Index auf generische Weise“ meinen? Meinen Sie damit, dass Sie verschiedene Namen für die Taste index in der Aktion haben möchten?

Ich glaube, ich verstehe jetzt, was du meinst.

Tut mir leid, dass ich gerade nicht viel kommentieren kann. Ich melde mich morgen zurück.

Ich verstehe das Problem jetzt. Etwas nachgehen.

Ich bin schnell auf einige Einschränkungen gestoßen, die damit verbunden sind, wie Redux von der Elm-Architektur abweicht.
Schade, dass ich sie vorher nicht verstanden habe!

  • Wenn die Komponente umschlossen ist, muss sie eine dispatch -Prop und keine Rückrufe akzeptieren. Das bedeutet, dass Sie entweder vermeiden müssen, Aktionsersteller zu Ihren Requisiten zu machen, und einfach dispatch() in den zusammengesetzten Komponenten verwenden, oder wir müssen ein bindActionCreators(Component, actionCreators) => Component einführen, das genau wie connect() ist verbindet sich aber nicht wirklich mit dem Store, sondern ersetzt lediglich action-creator-as-props-wanting-component durch this.props.dispatch -wanting-component.
  • Sie können keine Middleware-erfordernden Aktionen von umschlossenen Komponenten senden. Das ist ein Mist! Wenn ich den Zähler mit einer Liste verpacke, kann ich incrementAsync() nicht mehr versenden, weil function (dispatch, getState) { ... } , das ich gerade versendet habe, durch die Liste in { action: function (dispatch, getState) { ... } } umgewandelt wird – und bam! die Thunk-Middleware erkennt es nicht mehr.

Es gibt vielleicht nicht umständliche Lösungen dafür, aber ich sehe sie noch nicht.
Sehen Sie sich diesen Commit vorerst als Beispiel an (mit den oben beschriebenen Einschränkungen).

Hier ist der Code:

Komponenten/Counter.js

import React, { Component, PropTypes } from 'react';
import { increment, incrementIfOdd, incrementAsync, decrement } from '../actions/counter';

class Counter extends Component {
  render() {
    const { dispatch, counter } = this.props;
    return (
      <p>
        Clicked: {counter} times
        {' '}
        <button onClick={() => dispatch(increment())}>+</button>
        {' '}
        <button onClick={() => dispatch(decrement())}>-</button>
        {' '}
        <button onClick={() => dispatch(incrementIfOdd())}>Increment if odd</button>
        {' '}
        <button onClick={() => dispatch(incrementAsync())}>Increment async</button>
      </p>
    );
  }
}

Counter.propTypes = {
  dispatch: PropTypes.func.isRequired,
  counter: PropTypes.number.isRequired
};

export default Counter;

Komponenten/Liste.js

import React, { Component, PropTypes } from 'react';
import { addToList, removeFromList, performInList } from '../actions/list';

export default function list(mapItemStateToProps) {
  return function (Item) {
    return class List extends Component {
      static propTypes = {
        dispatch: PropTypes.func.isRequired,
        items: PropTypes.array.isRequired
      };

      render() {
        const { dispatch, items } = this.props;
        return (
          <div>
            <button onClick={() =>
              dispatch(addToList())
            }>Add counter</button>

            <br />
            {items.length > 0 &&
              <button onClick={() =>
                dispatch(removeFromList(items.length - 1))
              }>Remove counter</button>
            }
            <br />
            {this.props.items.map((item, index) =>
              <Item {...mapItemStateToProps(item)}
                    key={index}
                    dispatch={action =>
                      dispatch(performInList(index, action))
                    } />
            )}
          </div>
        )
      }
    }
  };
}

Aktionen/Counter.js

export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';

export function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

export function decrement() {
  return {
    type: DECREMENT_COUNTER
  };
}

export function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

export function incrementAsync(delay = 1000) {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment());
    }, delay);
  };
}

Aktionen/Liste.js

export const ADD_TO_LIST = 'ADD_TO_LIST';
export const REMOVE_FROM_LIST = 'REMOVE_FROM_LIST';
export const PERFORM_IN_LIST = 'PERFORM_IN_LIST';

export function addToList() {
  return {
    type: ADD_TO_LIST
  };
}

export function removeFromList(index) {
  return {
    type: REMOVE_FROM_LIST,
    index
  };
}

export function performInList(index, action) {
  return {
    type: PERFORM_IN_LIST,
    index,
    action
  };
}

Reducer/Counter.js

import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';

export default function counter(state = 0, action) {
  switch (action.type) {
  case INCREMENT_COUNTER:
    return state + 1;
  case DECREMENT_COUNTER:
    return state - 1;
  default:
    return state;
  }
}

Reducer/list.js

import { ADD_TO_LIST, REMOVE_FROM_LIST, PERFORM_IN_LIST } from '../actions/list';

export default function list(reducer) {
  return function (state = [], action) {
    const {
      index,
      action: innerAction
    } = action;

    switch (action.type) {
    case ADD_TO_LIST:
      return [
        ...state,
        reducer(undefined, action)
      ];
    case REMOVE_FROM_LIST:
      return [
        ...state.slice(0, index),
        ...state.slice(index + 1)
      ];
    case PERFORM_IN_LIST:
      return [
        ...state.slice(0, index),
        reducer(state[index], innerAction),
        ...state.slice(index + 1)
      ];
    default:
      return state;
    }
  }
}

Reducer/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);

const rootReducer = combineReducers({
  counterList
});

export default rootReducer;

Container/App.js

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

import Counter from '../components/Counter';
import list from '../components/list';

const CounterList = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(Counter);

export default connect(function mapStateToProps(state) {
  return {
    items: state.counterList
  };
})(CounterList);

cc @acdlite – hier ist ein Beispiel für ein aktuelles Middleware + React Redux-Design, das etwas zusammenbricht.
Wir können dies als nicht behoben deklarieren, aber vielleicht möchten Sie einen Blick darauf werfen, ob es eine Möglichkeit gibt, dies zu umgehen.

Ich habe mit der Verwendung eines Dienstcontainers (IoC) für React experimentiert und gestern dieses Test-Repo erstellt: https://github.com/magnusjt/react-ioc

Ich denke, es könnte möglicherweise einen Teil des Problems lösen, da Sie einen Aktionsersteller an den Counter weitergeben können, ohne dass CounterList davon erfährt. Dies ist möglich, weil der Aktionsersteller in den Konstruktor für Counter geht, nicht in die Requisiten.

Für jede neue Counter-Komponente, die Sie erstellen, können Sie einen anderen Aktionsersteller übergeben (indem Sie vielleicht einen Indexwert an den Aktionsersteller binden). Natürlich haben Sie immer noch das Problem, die Daten auf den Zähler zu bekommen. Ich bin mir noch nicht sicher, ob das etwas ist, was mit einem Servicecontainer gelöst werden könnte.

@gaearon , dein Beispiel sieht für mich ungefähr richtig aus. Sie müssen die Aktionsersteller passieren und den ganzen Weg nach unten versenden. Auf diese Weise können Sie Aktionen mit Funktionen höherer Ordnung ändern.

Ich bin mir nicht sicher, ob Ihr zweiter Punkt notwendig ist. Sie werden die Middleware wegen des neuen Nachrichtenformats vermissen, aber ein größeres Problem mit performInList ist, dass Sie die Abstraktion auf nur eine Liste nach oben beschränkt haben. @pe3 erwähnte eine Liste von Zählerlisten. Für eine solche willkürliche Abstraktion müssen Sie Aktionen meiner Meinung nach irgendwie verschachteln.

In diesem Ticket habe ich mir eine Möglichkeit ausgedacht, die Typen zu verschachteln:
https://github.com/rackt/redux/issues/897

Aber ich denke, grundlegender, Sie möchten die Aktionen vollständig verschachteln ...

Okay, ich habe es gerade probiert.

Ich habe die Dinge ziemlich vereinfacht. Hier ist die Zähler-App, bevor Sie diese ausgefallenen Sachen machen:

https://github.com/ccorcos/redux-lifted-reducers/blob/80295a09c4d04654e6b36ecc8bc1bfac4ae821c7/index.js#L49

Und hier ist es danach:

https://github.com/ccorcos/redux-lifted-reducers/blob/1fdafe8ed29303822018cde4973fda3305b43bb6/index.js#L57

Ich bin mir nicht sicher, ob "Lift" der richtige Begriff ist - ich weiß, dass es in der funktionalen Programmierung etwas bedeutet, aber für mich war es in Ordnung.

Wenn Sie eine Aktion aufheben, verschachteln Sie im Grunde eine Aktion in einer anderen.

const liftActionCreator = liftingAction => actionCreator => action => Object.assign({}, liftingAction, { nextAction: actionCreator(action) })

Und die verschachtelte Aktion wird durch Anheben des Reduzierstücks entfernt. Der Hubreduzierer wendet im Grunde den Unterreduzierer (der teilweise mit der entsprechenden Aktion angewendet wird) auf einen Untergrund an.

const liftReducer = liftingReducer => reducer => (state, action) => liftingReducer(state, action)((subState) => reducer(subState, action.nextAction))

Für den Komponentenlisten-Reduzierer habe ich also eine Aktion, die angibt, für welche Komponente an welchem ​​Index die Unteraktion gilt.

// list actions
const LIST_INDEX = 'LIST_INDEX'
function actOnIndex(i) {
  return {
    type: LIST_INDEX,
    index: i
  }
}

Und ich habe einen „High-Order“ (ein weiterer ausgefallener Begriff, der sich einfach richtig angefühlt hat, haha ​​;) Reducer, der den Sub-Reducer auf den entsprechenden Sub-Zustand anwendet.

const list = (state=[], action) => (reduce) => {
  switch (action.type) {
    case LIST_INDEX:
      let nextState = state.slice(0)
      nextState[action.index] = reduce(nextState[action.index])
      return nextState
    default:
      return state;
  }
}

Und alles, was übrig bleibt, ist, den Zählreduzierer in den Listenreduzierer zu "heben".

const reducer = combineReducers({
  counts: liftReducer(list)(count)
});

Und jetzt für die Liste der Zähler müssen wir nur die Aktionen aufheben, wenn wir sie an die Zähler weitergeben.

class App extends Component {
  render() {
    const counters = [0,1,2,3,4].map((i) => {
      return (
        <Counter count={this.props.state.counts[i]}
                 increment={liftActionCreator(actOnIndex(i))(increment)}
                 decrement={liftActionCreator(actOnIndex(i))(decrement)}
                 dispatch={this.props.dispatch}
                 key={i}/>
      )
    })
    return (
      <div>
        {counters}
      </div>
    );
  }
}

Ich denke, dies könnte mit der richtigen Fachsprache formalisiert werden. Ich denke, Linsen könnten hier auch für die High-Order Reducer verwendet werden, aber ich habe sie nie erfolgreich eingesetzt, haha.

Und ich nehme zurück, was ich im letzten Kommentar gesagt habe – @gaearon hat Recht. Wenn Sie die Aktion so verschachteln, verpassen Sie die Middleware, und Sie müssen den Versand ganz nach unten weiterleiten, damit Sie die Aktionsersteller manipulieren können. Um dies zu unterstützen, muss Redux möglicherweise alle Unteraktionen über die Middleware anwenden. Ein weiteres Problem ist das Initialisieren des Status in der Liste ...

Was Sie beschreiben, ist als Elm-Architektur bekannt. Siehe hier: https://github.com/gaearon/react-elmish-example

Alter, du bist immer einen Schritt voraus! Überschütte mich mit Links zu coolen Sachen :+1:

Sie können keine Middleware-erfordernden Aktionen von umschlossenen Komponenten senden. Das ist ein Mist! Wenn ich einen Zähler mit einer Liste umschließe, kann ich incrementAsync() nicht mehr versenden, da die Funktion (dispatch, getState) { ... }, die ich gerade versendet habe, in { action: function (dispatch, getState) { ... } } umgewandelt wird die Liste – und bam! die Thunk-Middleware erkennt es nicht mehr.

@gaearon wie wäre es mit dieser Lösung? Anstelle des generischen Reduzierers, der sich selbst als untergeordneter Reduzierer bezeichnet

case PERFORM_IN_LIST:
      return [
        ...state.slice(0, index),
        reducer(state[index], innerAction),
        ...state.slice(index + 1)
      ];

Stellen Sie dem Geschäft eine spezielle Methode dispatchTo(reducer, state, action, callback) zur Verfügung, die sich wie dispatch verhält, außer dass sie direkt an einen untergeordneten Reduzierer (über alle konfigurierten Middelwares) sendet und den Rückruf bei jeder Änderung des untergeordneten Zustands benachrichtigt

export default function list(reducer, dispatchTo) {
  return function (state = [], action) {
    ...
    case PERFORM_IN_LIST:
      dispatchTo(reducer, state[index], innerAction, newState =>
         [
           ...state.slice(0, index),
           newState,
           ...state.slice(index + 1)
        ]);
       default:
          return state;
    }
  }
}

Mir ist nicht bekannt, ob dies in Redux machbar ist. Eine Idee ist, dispatchTo mit einer internen store.derive(reducer, state) -Methode zu implementieren, die einen untergeordneten Speicher für diesen Teil des Zustandsbaums zurückgibt, der mit einigen Middlewares als Stammspeicher konfiguriert ist. zum Beispiel

function dispatchTo(reducer, state, action, callback) {
  const childStore = store.derive(reducer, state)
  childStore.subscribe(() => setRootState( callback(getState() ))
  childStore.dispatch(action)
}

Dies ist nur eine Idee, wie ich sagte, ich kenne die Interna von Redux nicht, also habe ich vielleicht etwas verpasst

BEARBEITEN
_Wahrscheinlich wird dies umständlich, da Reducer-Methoden synchron sein sollen. Wenn Sie eine Methode asynchron machen, bedeutet dies, dass die gesamte Kette auch asynchron sein muss.
Vielleicht ist die beste Lösung, die store.derive(reducer) -Methode direkt zu erklären und generische Reduzierer mit einer Art Store-Komposition zu bauen_

Das ist zu kompliziert und lohnt sich meiner Meinung nach nicht. Wenn Sie es auf diese Weise tun möchten, verwenden Sie einfach nicht die Middleware (oder verwenden Sie eine alternative Implementierung von applyMiddleware ) und Sie sind fertig.

Außerdem schließe ich, weil wir nicht beabsichtigen, diesbezüglich tätig zu werden.

@gaearon zur Diskussion:
das Codebeispiel, das Sie in diesem Commit angehängt haben: https://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4

Falls ich 2 Listen von Zählern habe (oder sogar separate 'Modelle'), würde das Senden addToList beiden Listen Artikel hinzufügen, da die Aktionstypen gleich sind.

// reducers/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);
const counterList2 = list(counter);

const rootReducer = combineReducers({
  counterList,
  counterList2
});

export default rootReducer;

Wie hilft das reducers/list hier? Müssen Sie Aktionstypen oder so etwas nicht voranstellen?

Falls ich 2 Listen von Zählern habe (oder sogar separate 'Modelle'), würde das Versenden von addToList ein Element zu beiden Listen hinzufügen, da die Aktionstypen gleich sind.

Bitte schauen Sie sich a83002aed8e36f901ebb5f139dd14ce9c2e4cab4 genau an. Es verschachtelt Aktionen. dispatch , das von der Containerkomponente weitergegeben wird, umschließt Aktionen in performInList -Aktionen. Dann wird die innere Aktion im Reduzierstück abgerufen. So funktioniert Elm Architecture.

@gaearon vielleicht fehlt mir etwas, aber zusammen mit dem oben erwähnten zusätzlichen counterList2 -Reduzierer aktualisiert diese Benutzeroberfläche immer noch beide Listen für jede Aktion (was je nach Aufbau erwartet wird, aber was ist die Lösung?):

// reducers/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);
const counterList2 = list(counter);

const rootReducer = combineReducers({
  counterList,
  counterList2
});

export default rootReducer;


// containers/App.js

import React from 'react';

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

import counter from '../components/Counter';
import list from '../components/list';

let CounterList = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(counter);

CounterList = connect(function mapStateToProps(state) {
  return {
    items: state.counterList
  };
})(CounterList);

let CounterList2 = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(counter);

CounterList2 = connect(function mapStateToProps(state) {
  return {
    items: state.counterList2
  };
})(CounterList2);


export default class App extends React.Component {
  render() {
    return (
      <div>
        <CounterList />
        <CounterList2 />
      </div>
    )
  }
}

@elado Sie müssen es erneut in eine Liste einschließen, damit die Aktionen für die beiden Listen nicht kollidieren, genau wie wir es mit der Liste der Zähler getan haben

@ccorcos

um es wieder in eine Liste zu packen

was genau wickeln?

@ccorcos
Ich habe das Beispiel hier hochgeladen: http://elado.s3-website-us-west-1.amazonaws.com/redux-counter/
Es hat Quellkarten

Ich bin mir immer noch nicht ganz sicher, was du meinst. Wie gesagt - das aktuelle Verhalten ist das erwartete, da die Aktionsnamen gleich sind und es im Aktionsersteller keinen Hinweis darauf gibt, in welcher Liste sie ausgeführt werden soll, sodass die Aktion auf allen Reduzierern ausgeführt wird, was sich letztendlich auf beide Listen auswirkt.

Ich kenne also die eigentlichen Redux-Funktionen nicht sehr gut, aber ich bin mit elm sehr vertraut.

In diesem neuen Beispiel binden wir die Aktionsersteller nicht auf der obersten Ebene. Stattdessen übergeben wir die Versandfunktion an die unteren Komponenten, und diese unteren Komponenten können eine Aktion an die Versandfunktion weitergeben.

Damit die Abstraktion gut funktioniert und wir keine Aktionskollisionen haben, übergibt die "listOf"-Komponente den Dispatch an ihre untergeordneten Elemente, indem sie tatsächlich eine Funktion übergibt, die die Aktion in ein Format verpackt, das die Listenkomponente verstehen kann.

children.map((child, i) => {
  childDispatch = (action) => dispatch({action, index: i})
  // ...

Jetzt können Sie also listOf(counter) oder listOf(listOf(counter)) zusammensetzen, und wenn Sie eine Komponente namens pairOf erstellen möchten, müssen Sie sicherstellen, dass die Aktionen umschlossen werden, wenn Sie sie weitergeben. Im Moment rendert die App -Komponente sie einfach nebeneinander, ohne die Dispatch-Funktionen zu umbrechen, sodass Sie Aktionskollisionen haben.

@ccorcos

Danke. Abschließend lässt sich sagen, dass offensichtlich keine "Magie" passiert, Aktionen benötigen alle Informationen, die sie benötigen, um dem Reduzierer mitzuteilen, auf welcher Instanz die Aktion ausgeführt werden soll. Wenn es sich um listOf(listOf(counter)) handelt, benötigt die Aktion beide Indizes des 2d-Arrays. listOf(listOf(counter)) kann funktionieren, aber alle Zähler in einer einzigen flachen Liste zu haben, die durch eine eindeutige ID indiziert ist, die das einzige ist, was in einer Aktion übergeben wird, scheint flexibler zu sein.

Es sieht so aus, als ob der einzige Weg, eine flexible und komplexe Redux-App zu erstellen, darin besteht, alle Entitäten im System nach ID im Store indizieren zu lassen. Jeder andere einfachere Ansatz, der manchmal in Beispielen angeführt wird, stößt schnell an seine Grenzen. Dieser Index ist fast ein Spiegel einer relationalen DB.

Die Aktion benötigt beide Indizes des 2d-Arrays

es klingt wie dein Denken und Handeln sieht so aus:

{type: 'increment', index:[0 5]}

aber es sollte wirklich so aussehen:

{type:'child', index: 0, action: {type: 'child', index: 5, action: {type: 'increment'}}}

Auf diese Weise können Sie für immer listOf(listOf(listOf(...listOf(counter)...))) machen!

Das kommt übrigens alles von Elm. Schauen Sie sich das Elm-Architektur-Tutorial an

Ich bin etwas langsam auf der Party, aber ich sehe nicht, wo das "mit Middleware nicht funktioniert"? Wenn Sie unendliche Verschachtelung als @ccorcos anbieten , können Sie nicht einfach Middleware verdrahten, um die Verschachtelung zu handhaben? Oder sprechen wir ausschließlich über redux-thunk , wo verschachtelte Aktionen Verrücktheit bedeuten würden?

Woher weiß die Middleware, ob sie die Aktion so interpretieren soll, wie sie ist, oder ob sie nach verschachtelten Aktionen suchen soll?

Aha.

@gaearon

Hallo.

Ich bin mir nicht sicher, ob ich die Entschließung zu diesem Thema verstanden habe, und würde mich sehr über eine einfache Antwort freuen.

Ist es _keine Middleware (und im Grunde den größten Teil des Redux-Ökosystems) zu verwenden, wenn Sie mehrere Kopien von Komponenten auf derselben Seite haben_? Ich habe auch nicht gesehen, ob jemand auf den Multireducer- Vorschlag reagiert hat.

Jede Aufklärung würde helfen.

Nein, das ist nicht die Auflösung. Beim Thread geht es nicht darum, mehrere Identitäten einer Komponente auf der Seite zu haben. Um dies zu implementieren, können Sie einfach eine ID in der Aktion übergeben. In dem Thread ging es darum, _eine generische Funktion dafür zu schreiben_. Was leider nicht mit dem Konzept der Middleware kollidiert.

Danke schön.

Hallo an alle,
Vielleicht habe ich dieses Problem gelöst, aber ich bevorzuge deku gegenüber react ^^

Ich würde mich freuen, wenn mir jemand seinen Einblick in dieses Experiment geben würde, insbesondere in Bezug auf die taskMiddleware -Idee. @gaearon hast du etwas Zeit, es dir anzusehen? :Zunge:

Liste der Zähler

Danke!

Ich möchte nur klarstellen, dass keine dieser Lösungen darauf abzielt, einen Anfangszustand zu handhaben.
Könnte dies irgendwie erreicht werden @gaearon ? Zulassen, dass der obige Listenreduzierer eine anfängliche Liste von Zählern hat?

Warum sollte das problematisch sein? Ich stelle mir vor, Sie könnten die untergeordneten Reduzierer mit dem Status undefined aufrufen und diese Werte verwenden.

Wenn das Geschäft mit dem ersten Versand initialisiert wird, muss ich (für meine interne Logik) einen Anfangszustand für diese Liste haben, und es sollte eine vordefinierte Liste von Zählern sein (der Typ der Elemente der Liste).

Etwas wie das?

export default function list(reducer) {
  return function (state = [

    // e.g. 2 counters with default values
    reducer(undefined, {}),
    reducer(undefined, {}),

      ], action) {
    const {
      index,
      action: innerAction
    } = action;
   // ...
  }
}

Sie könnten dies auch zu einem Argument für list machen, wenn Sie möchten

Das scheint wirklich kompliziert zu sein, nur damit ich mehrere gleiche Komponenten auf einer Seite haben kann.

Ich beschäftige mich immer noch mit Redux, aber das Erstellen mehrerer Stores scheint viel einfacher zu sein, auch wenn es kein empfohlenes Redux-Nutzungsmuster ist.

@deevus : Lass dich von einigen dieser Diskussionen nicht abschrecken. Es gibt eine Reihe von Leuten in der Redux-Community, die sehr auf funktionale Programmierung ausgerichtet sind, und obwohl einige der in diesem Thread diskutierten Konzepte und andere ähnliche einen Wert haben, neigen sie auch dazu, so etwas wie einen Versuch zu sein, nach dem "Perfekten" zu streben. , anstatt nur "gut".

Sie können im Allgemeinen mehrere Instanzen einer Komponente auf einer Seite haben. Das Ziel dieser Diskussion ist die willkürliche Zusammensetzung verschachtelter Komponenten, was interessant ist, aber auch nicht etwas, was die meisten Apps tun müssen.

Wenn Sie darüber hinaus spezielle Bedenken haben, ist Stack Overflow normalerweise ein guter Ort, um Fragen zu stellen. Außerdem hat die Reactiflux-Community auf Discord eine Reihe von Chat-Kanälen, die sich der Diskussion über React und verwandte Technologien widmen, und es gibt immer einige Leute, die bereit sind, zu reden und zu helfen.

@Markerikson Danke. Ich werde versuchen, Hilfe von Reactiflux auf Discord zu bekommen.

Dem nachgehen:
Ich habe eine skalierbare Möglichkeit gefunden, Reduzierer in meinem Projekt wiederzuverwenden, sogar Reduzierer innerhalb von Reduzierern wiederzuverwenden.

Das Namespace-Paradigma

Es ist das Konzept, dass Aktionen, die auf einen von vielen "partiellen" Reduzierern wirken, eine "Namespace"-Eigenschaft besitzen, die bestimmt, welcher Reduzierer die Aktion behandelt, im Gegensatz zu _allen_ Reduzierern, die sie behandeln, weil sie dieselbe Aktion type abhören (Beispiel in https://github.com/reactjs/redux/issues/822#issuecomment-172958967)

Aktionen, die keinen Namensraum enthalten, werden jedoch weiterhin an alle partiellen Reduzierer innerhalb anderer Reduzierer weitergegeben

Angenommen, Sie haben den partiellen Reducer A und mit einem Anfangszustand von A(undefined, {}) === Sa und einen Reducer B mit einem Anfangszustand von B(undefined, {}) === { a1: Sa, a2: Sa } , wobei die Tasten a1 und a2 sind Instanzen von A .

Eine Aktion mit dem Namensraum ['a1'] (* Namensräume sind immer geordnete Arrays von Zeichenfolgen, die dem Schlüssel des Zustands zum partiellen Reduzierer ähneln), die auf B wird, erzeugt das folgende Ergebnis

const action = {
  type: UNIQUE_ID,
  namespace: ['a1']
};

B(undefined, action) == { a1: A(undefined, action*), a2: Sa }

Und das Gegenbeispiel einer Aktion ohne Namespace

const action = {
  type: UNIQUE_ID
};


B(undefined, action) == { a1: A(undefined, action), a2: A(undefined, action) }

Vorbehalte

  • Wenn A die gegebene Aktion nicht handhabt (es erschöpft den Reducer), sollte es den gleichen Zustand zurückgeben, das bedeutet, dass für eine Aktion p die für den Reducer A unhandlich ist das Ergebnis von B(undefined, p) sollte { a1: Sa, a2: Sa } sein, was übrigens der Anfangszustand von B ist
  • Die Aktion, die an partielle Reduzierer weitergegeben wird (oben als action* bezeichnet), muss aus dem Namensraum entfernt werden, der zum Eingrenzen des Bereichs verwendet wird. Wenn also die an B übergebene Aktion { type: UNIQUE_ID, namespace: ['a1'] } war, dann ist die an A weitergegebene Aktion { type: UNIQUE_ID, namespace: [] }
  • Um diese Struktur zu erreichen, müssen alle Reducer im Store Namespace-Aktionen handhaben, wenn dieser Punkt erfüllt ist, gibt es keine Begrenzung dafür, wie viele Namespaces Sie verwenden und wie viele partielle Reducer Sie verschachteln können

Um dies zu erreichen, habe ich Pseudocode für den Umgang mit Namespaces in Ihrem Reducer entwickelt. Damit dies funktioniert, müssen wir vorher wissen, ob ein Reduzierer eine Aktion verarbeiten kann und wie viele Teilreduzierer im Reduzierer vorhanden sind.

(state = initialState, { ...action, namespace = [] }) => {
    var partialAction = { ...action, namespace: namespace.slice(1) };
    var newState;
    if (reducerCanHandleAction(reducer, action) and namespaceExistsInState(namespace, state)) {
        // apply the action to the matching partial reducer
        newState = {
            ...state,
            [namespace]: partialReducers[namespace](state[namespace], partialAction)
        };
    } else if (reducerCantHandleAction(reducer, action) {
        // apply the action to all partial reducers
        newState = Object.assign(
            {},
            state,
            ...Object.keys(partialReducers).map(
                namespace => partialReducers[namespace](state[namespace], action)
            )
        );
    } else {
        // can't handle the action
        return state;
    }

    return reducer(newState, action);
}

Es liegt an Ihnen, vorher zu entscheiden, ob der Reducer die Aktion verarbeiten kann oder nicht. Ich verwende eine Objektkarte, in der die Aktionstypen die Schlüssel und die Handlerfunktionen die Werte sind.

Ich bin vielleicht etwas spät im Spiel, aber ich habe einige generische Reduzierungen geschrieben, die helfen könnten:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1

Ich denke, mutilreducer ist eine großartige Implementierung

@jeffhtli multireducer ist keine gute Lösung, da es keine undefinierte Anzahl von Reduzierern zulässt, sondern Sie präventiv auffordert, eine statische Reduziererliste zu erstellen
Stattdessen habe ich ein kleines Projekt erstellt, um dieses Problem mit UUIDs für jede Instanz von Komponenten und einem eindeutigen Zustand für jede UUID zu lösen
https://github.com/eloytoro/react-redux-uuid

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

vslinko picture vslinko  ·  3Kommentare

captbaritone picture captbaritone  ·  3Kommentare

ilearnio picture ilearnio  ·  3Kommentare

mickeyreiss-visor picture mickeyreiss-visor  ·  3Kommentare

ramakay picture ramakay  ·  3Kommentare