Redux: Comment créer une liste générique en tant que réducteur et amplificateur de composants ?

Créé le 30 sept. 2015  ·  50Commentaires  ·  Source: reduxjs/redux

Bonjour. Quel serait un bon moyen d'étendre l' exemple de compteur à une liste dynamique de compteurs indépendants ?

Par dynamique, je veux dire que dans l'interface utilisateur à la fin de la liste des compteurs, il y aurait des boutons + et - pour ajouter un nouveau compteur à la liste ou supprimer le dernier.

Idéalement, le contre-réducteur et le composant resteraient tels quels. Comment créer une liste généralisée magasin+composant pour collecter tout type d'entités ? Serait-il possible de généraliser encore plus la liste store+component pour prendre à la fois les compteurs et les todo-items de l' exemple todomvc ?

Ce serait bien d'avoir quelque chose comme ça dans les exemples.

examples

Commentaire le plus utile

Comment créer une liste généralisée magasin+composant pour collecter tout type d'entités ? Serait-il possible de généraliser encore plus la liste store+component pour prendre à la fois les compteurs et les todo-items de l'exemple todomvc ?

Oui, certainement possible.
Vous voudriez créer un réducteur d'ordre supérieur et un composant d'ordre supérieur.

Pour un réducteur d'ordre supérieur, voir l'approche ici :

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

(Je ne l'ai pas exécuté mais cela devrait fonctionner avec des changements minimes.)

Tous les 50 commentaires

Je suis d'accord c'est un bon exemple.
Redux est similaire ici à Elm Architecture , alors n'hésitez pas à vous inspirer des exemples qui s'y trouvent.

Comment créer une liste généralisée magasin+composant pour collecter tout type d'entités ? Serait-il possible de généraliser encore plus la liste store+component pour prendre à la fois les compteurs et les todo-items de l'exemple todomvc ?

Oui, certainement possible.
Vous voudriez créer un réducteur d'ordre supérieur et un composant d'ordre supérieur.

Pour un réducteur d'ordre supérieur, voir l'approche ici :

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

(Je ne l'ai pas exécuté mais cela devrait fonctionner avec des changements minimes.)

Merci - je vais essayer de faire fonctionner cette approche.

Je me demande toujours si je peux réutiliser la fonctionnalité de liste via combineReducers pour avoir à la fois une liste de compteurs et une liste d'éléments à faire. Et si cela avait du sens. Mais je vais certainement essayer.

Je me demande toujours si je peux réutiliser la fonctionnalité de liste via combineReducers pour avoir à la fois une liste de compteurs et une liste de todo-items. Et si cela avait du sens. Mais je vais certainement essayer.

Oui tout à fait:

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

@gaearon comment l'action de liste devrait-elle obtenir son index ?

Nous avons réussi à créer un réducteur d'ordre supérieur avec vos instructions, mais nous avons du mal avec le composant d'ordre supérieur. Actuellement, notre composant n'est pas assez générique pour être utilisé avec d'autres composants que Counter. Notre problème est de savoir comment ajouter l'index aux actions de manière générique.

Vous pouvez voir notre solution ici : https://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c

J'ai ajouté un commentaire au commit pour mettre en évidence la partie problématique.

Ce serait formidable d'avoir un réducteur de liste général + composant qui peut faire des listes de listes de compteurs.

Actuellement, notre composant n'est pas assez générique pour être utilisé avec d'autres composants que Counter. Notre problème est de savoir comment ajouter l'index aux actions de manière générique.

Pouvez-vous expliquer ce que vous entendez par "ajouter l'index de manière générique" ? Voulez-vous dire que vous voulez avoir des noms différents pour la clé index dans l'action ?

Je pense que je comprends ce que tu veux dire maintenant.

Désolé, je ne peux pas commenter beaucoup pour le moment. Je reviens demain.

Je comprends le problème maintenant. En regardant dedans.

Je me suis rapidement heurté à certaines limitations inhérentes à la façon dont Redux s'écarte de l'architecture Elm.
Dommage que je ne les ai pas compris avant !

  • Au moment où le composant est encapsulé, il doit accepter un accessoire dispatch et non des rappels. Cela signifie que vous devez soit éviter de faire des créateurs d'action vos accessoires et utiliser simplement dispatch() dans les composants composés, soit nous devons introduire un bindActionCreators(Component, actionCreators) => Component qui ressemble à connect() mais ne se connecte pas réellement au magasin, remplaçant simplement action-creator-as-props-wanting-component par this.props.dispatch -wanting-component.
  • Vous ne pouvez pas envoyer d'actions nécessitant un middleware à partir de composants encapsulés. C'est une déception ! Si j'enveloppe le compteur avec une liste, je ne peux plus envoyer incrementAsync() parce que function (dispatch, getState) { ... } que je viens d'envoyer est transformé en { action: function (dispatch, getState) { ... } } par la liste - et bam ! le middleware thunk ne le reconnaît plus.

Il peut y avoir des solutions non gênantes à cela, mais je ne les vois pas encore.
Pour l'instant, veuillez voir ce commit comme exemple (avec les limitations décrites ci-dessus).

Voici le code :

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

composants/list.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>
        )
      }
    }
  };
}

actions/compteur.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);
  };
}

actions/list.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
  };
}

réducteurs/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;
  }
}

réducteurs/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;
    }
  }
}

réducteurs/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;

conteneurs/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 - voici un exemple de la conception actuelle du middleware + React Redux en panne quelque peu.
Nous pouvons déclarer cela comme wontfix mais peut-être voudriez-vous jeter un coup d'œil s'il y a un moyen d'esquiver cela.

J'ai expérimenté l'utilisation d'un conteneur de service (IoC) pour React et j'ai créé ce test-repo hier : https://github.com/magnusjt/react-ioc

Je pense que cela pourrait potentiellement résoudre une partie du problème, puisque vous pouvez transmettre un créateur d'action au compteur sans que CounterList le sache. Ceci est possible car le créateur de l'action va dans le constructeur de Counter, pas dans les accessoires.

Pour chaque nouveau composant Counter que vous créez, vous pouvez transmettre un créateur d'action différent (peut-être en liant une valeur d'index au créateur d'action). Bien sûr, vous avez toujours le problème d'amener les données au comptoir. Je ne sais pas encore si c'est quelque chose qui pourrait être résolu avec un conteneur de service.

@gaearon , votre exemple me semble à peu près juste. Vous devez passer les créateurs d'action et expédier tout le long. De cette façon, vous pouvez altérer les actions avec des fonctions de haut niveau.

Je ne suis pas sûr que votre deuxième point soit nécessaire. Vous manquerez le middleware à cause du nouveau format de message, mais un problème plus important avec performInList est que vous avez limité l'abstraction à une seule liste. @pe3 a mentionné une liste de listes de compteurs. Pour une abstraction arbitraire comme celle-ci, je pense que vous devrez imbriquer les actions d'une manière ou d'une autre.

Dans ce ticket, je propose un moyen d'imbriquer les types :
https://github.com/rackt/redux/issues/897

Mais je pense que plus fondamentalement, vous voudrez imbriquer entièrement les actions ....

Ok, je viens de m'y mettre.

J'ai un peu simplifié les choses. Voici l'application de compteur avant de faire ce truc fantaisiste :

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

Et le voici après :

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

Je ne suis pas sûr que "lift" soit le terme approprié - je sais que cela signifie quelque chose dans la programmation fonctionnelle, mais je me sentais bien.

Fondamentalement, en levant une action, vous imbriquez une action dans une autre.

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

Et l'action imbriquée se détache en soulevant le réducteur. Le réducteur de levage applique essentiellement le sous-réducteur (qui est partiellement appliqué avec l'action appropriée) à un sous-état.

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

Donc, pour le réducteur de liste de composants, j'ai une action qui spécifie à quel composant à quel index la sous-action s'applique.

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

Et j'ai un réducteur "d'ordre élevé" (un autre terme fantaisiste qui me semblait juste, haha;) qui applique le sous-réducteur au sous-état approprié.

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;
  }
}

Et tout ce qui reste est de "soulever" le réducteur de compte dans le réducteur de liste.

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

Et maintenant, pour la liste des compteurs, nous avons juste besoin de lever les actions au fur et à mesure que nous les transmettons aux compteurs.

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

Je pense que cela pourrait être plus formalisé avec un jargon approprié. Je pense que les lentilles pourraient également être utilisées ici pour les réducteurs d'ordre élevé, mais je ne les ai jamais utilisées avec succès, haha.

Et je retire ce que j'ai dit dans le dernier commentaire -- @gaearon a raison. En imbriquant l'action comme celle-ci, vous allez manquer le middleware, et vous devez transmettre la répartition tout en bas pour pouvoir manipuler les créateurs d'action. Peut-être que pour supporter cela, Redux devra appliquer toutes les sous-actions via le middleware. De plus, un autre problème est l'initialisation de l'état dans la liste...

Ce que vous décrivez est connu sous le nom d'Elm Architecture. Veuillez voir ici : https://github.com/gaearon/react-elmish-example

Mec, tu as toujours une longueur d'avance ! Douchez-moi avec des liens vers des trucs sympas :+1:

Vous ne pouvez pas envoyer d'actions nécessitant un middleware à partir de composants encapsulés. C'est une déception ! Si j'enveloppe le compteur avec une liste, je ne peux plus envoyer incrementAsync() car la fonction (dispatch, getState) { ... } que je viens d'envoyer est transformée en { action: function (dispatch, getState) { ... } } par la liste et bam ! le middleware thunk ne le reconnaît plus.

@gaearon que diriez-vous de cette solution ? au lieu que le réducteur générique s'appelle le réducteur enfant comme celui-ci

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

fournir au magasin une méthode spéciale dispatchTo(reducer, state, action, callback) qui agit comme dispatch sauf qu'elle est envoyée directement à un réducteur enfant (via tous les middelwares configurés) et notifie le rappel à chaque modification de l'état enfant

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;
    }
  }
}

Je ne sais pas si cela est faisable dans Redux. Une idée consiste à implémenter dispatchTo en utilisant une méthode interne store.derive(reducer, state) qui renverrait un magasin enfant pour cette partie de l'arborescence d'état configurée avec certains middlewares comme magasin racine. par example

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

Ceci est juste une idée car j'ai dit que je ne connaissais pas les composants internes de Redux, alors peut-être que j'ai raté quelque chose

ÉDITER
_probablement cela va être gênant puisque les méthodes de réduction sont supposées être synchrones. rendre une méthode asynchrone implique que toute la chaîne doit également être asynchrone.
Peut-être que la meilleure solution est d'exposer directement la méthode store.derive(reducer) et de construire des réducteurs génériques en utilisant une sorte de composition Store_

C'est trop compliqué et ça n'en vaut pas la peine. Si vous souhaitez le faire de cette façon, n'utilisez pas le middleware (ou utilisez une implémentation alternative de applyMiddleware ) et vous êtes prêt.

De plus, je ferme parce que nous ne prévoyons pas d'agir à ce sujet.

@gaearon pour le plaisir de la discussion :
l'exemple de code que vous avez joint dans ce commit : https://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4

Dans le cas où j'ai 2 listes de compteurs (ou même des "modèles" séparés), l'envoi addToList ajouterait un élément aux deux listes, car les types d'action sont les mêmes.

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

alors, comment le reducers/list aide-t-il ici ? N'avez-vous pas besoin de préfixer les types d'action ou quelque chose ?

Dans le cas où j'ai 2 listes de compteurs (ou même des "modèles" séparés), l'envoi de addToList ajouterait un élément aux deux listes, car les types d'action sont les mêmes.

Veuillez regarder attentivement a83002aed8e36f901ebb5f139dd14ce9c2e4cab4. Il imbrique des actions. dispatch qui est transmis depuis le composant de conteneur encapsule les actions dans l'action performInList . Ensuite, l'action intérieure est récupérée dans le réducteur. C'est à peu près ainsi que fonctionne Elm Architecture.

@gaearon peut-être qu'il me manque quelque chose, mais avec le réducteur supplémentaire counterList2 mentionné ci-dessus, cette interface utilisateur met toujours à jour les deux listes pour chaque action (ce qui est attendu en fonction de la façon dont elle est construite, mais quelle est la solution ?):

// 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 vous devrez l'envelopper à nouveau dans une liste afin que les actions ne se heurtent pas pour les deux listes, de la même manière que nous l'avons fait avec la liste des compteurs

@ccorcos

pour l'envelopper à nouveau dans une liste

envelopper quoi exactement ?

@ccorcos
J'ai téléchargé l'exemple ici : http://elado.s3-website-us-west-1.amazonaws.com/redux-counter/
Il a des cartes sources

Je ne suis toujours pas sûr de ce que vous vouliez dire. Comme je l'ai dit - le comportement actuel est celui attendu car les noms d'action sont les mêmes, et il n'y a aucune indication dans le créateur d'action dans quelle liste l'exécuter, il exécute donc l'action sur tous les réducteurs, ce qui affecte finalement les deux listes.

Je ne connais donc pas très bien les fonctions réelles de Redux, mais je connais très bien elm.

Dans ce nouvel exemple, nous ne lions pas les créateurs d'action au niveau supérieur. Au lieu de cela, nous transmettons la fonction de répartition aux composants inférieurs et ces composants inférieurs peuvent transmettre une action à la fonction de répartition.

Pour que l'abstraction fonctionne bien afin d'éviter les collisions d'actions, lorsque le composant "listOf" transmet l'envoi à ses enfants, il transmet en fait une fonction qui encapsule l'action dans un format que le composant de liste peut comprendre.

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

Alors maintenant, vous pouvez composer listOf(counter) ou listOf(listOf(counter)) et si vous voulez créer un composant appelé pairOf , vous devez vous assurer d'envelopper les actions lorsque vous les transmettez. À l'heure actuelle, le composant App les rend juste côte à côte sans envelopper les fonctions de répartition, vous avez donc des collisions d'action.

@ccorcos

Merci. Donc, pour conclure, il n'y a évidemment pas de "magie" qui se passe, les actions ont besoin de toutes les informations dont elles ont besoin pour indiquer au réducteur sur quelle instance effectuer l'action. Si c'est un listOf(listOf(counter)) l'action aura besoin des deux indices du tableau 2d. listOf(listOf(counter)) peut fonctionner mais avoir tous les compteurs dans une seule liste plate indexée par un ID unique qui est la seule chose transmise dans une action semble plus flexible.

Il semble que la seule façon de créer une application Redux flexible et complexe consiste à indexer toutes les entités du système par ID dans le magasin. Toute autre approche plus simple, parfois donnée en exemples, atteindra rapidement ses limites. Cet index est presque un miroir d'une base de données relationnelle.

l'action aura besoin des deux indices du tableau 2d

cela ressemble à ce que vous pensez et l'action ressemble à :

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

mais ça devrait vraiment ressembler à :

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

De cette façon, vous pouvez faire un listOf(listOf(listOf(...listOf(counter)...))) pour toujours !

Tout cela vient d'Elm, btw. Consultez le didacticiel d'architecture Elm

Je suis un peu lent à la fête, mais je ne vois pas en quoi cela "ne marche pas avec le middleware" ? Si vous proposez une imbrication infinie en tant que @ccorcos , ne pouvez-vous pas simplement connecter un middleware pour gérer l'imbrication? Ou parlons-nous exclusivement de redux-thunk où les actions imbriquées signifieraient bizarrerie ?

Comment le middleware saurait-il s'il faut interpréter l'action telle quelle ou rechercher des actions imbriquées ?

Je vois.

@gaearon

Salut.

Je ne suis pas sûr d'avoir compris la résolution sur la question et j'apprécierais vraiment une réponse simple.

Est-ce _n'utilisez pas de middleware (et fondamentalement la plupart de l'écosystème Redux), si vous avez plusieurs copies de composant sur la même page_ ? Je n'ai pas non plus vu si quelqu'un a répondu à la suggestion de multiréducteur .

Toute clarification aiderait.

Non, ce n'est pas la résolution. Le fil ne consiste pas à avoir plusieurs identités d'un composant sur la page. Pour implémenter cela, vous pouvez simplement passer un ID dans l'action. Le fil portait sur _l'écriture d'une fonction générique pour faire ça_. Ce qui, malheureusement, se heurte au concept de middleware.

Merci.

Bonjour à tous,
J'ai peut-être résolu ce problème, mais je préfère utiliser deku plutôt react ^^

Je serais heureux si quelqu'un me donnait son avis sur cette expérience, en particulier sur l'idée de taskMiddleware . @gaearon avez-vous le temps de le vérifier ? :langue:

Liste des compteurs

Merci!

Je veux juste préciser qu'aucune de ces solutions ne vise à gérer un état initial.
Cela pourrait-il être réalisé d'une manière ou d'une autre @gaearon ? Permettre au réducteur de liste ci-dessus d'avoir une liste initiale de compteurs ?

Pourquoi serait-ce problématique ? J'imagine que vous pourriez appeler les réducteurs enfants avec l'état undefined et utiliser ces valeurs.

Lorsque le magasin est initialisé avec la première expédition, j'ai besoin (pour ma logique interne) d'avoir un état initial pour cette liste, et il devrait s'agir d'une liste prédéfinie de compteurs (le type des éléments de la liste)

Quelque chose comme ça?

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;
   // ...
  }
}

Vous pouvez également en faire un argument pour list si vous le souhaitez

Cela semble vraiment compliqué juste pour que je puisse avoir plusieurs du même composant sur une page.

Je suis toujours en train de comprendre Redux, mais la création de plusieurs magasins semble beaucoup plus simple, même si ce n'est pas un modèle d'utilisation recommandé de Redux.

@deevus : ne laissez pas certaines de ces discussions vous effrayer. Il y a un certain nombre de personnes dans la communauté Redux qui sont très orientées vers la programmation fonctionnelle, et bien que certains des concepts discutés dans ce fil et d'autres similaires aient de la valeur, ils ont aussi tendance à être une tentative d'aller vers le "parfait" , plutôt que le simple "bon".

Vous pouvez totalement avoir plusieurs instances d'un composant sur une page, en général. Ce que cette discussion vise, c'est la composition arbitraire de composants imbriqués, ce qui est intéressant, mais pas non plus quelque chose que la plupart des applications devront faire.

Si vous avez des préoccupations spécifiques au-delà de cela, Stack Overflow est généralement un bon endroit pour poser des questions. De plus, la communauté Reactiflux sur Discord a un tas de canaux de discussion dédiés à discuter de React et des technologies associées, et il y a toujours des gens qui sont prêts à parler et à aider.

@markerikson Merci. Je vais essayer d'obtenir de l'aide de Reactiflux sur Discord.

Suite à cela:
J'ai trouvé un moyen évolutif de réutiliser des réducteurs dans mon projet, même de réutiliser des réducteurs dans des réducteurs.

Le paradigme de l'espace de noms

C'est le concept selon lequel les actions qui agissent sur l'un des nombreux réducteurs "partiels" détiennent une propriété "espace de noms" qui détermine quel réducteur gérera l'action contrairement à _tous_ les réducteurs la gérant parce qu'ils écoutent la même action type (exemple dans https://github.com/reactjs/redux/issues/822#issuecomment-172958967)

Cependant, les actions qui ne contiennent pas d'espace de noms seront toujours propagées à tous les réducteurs partiels dans d'autres réducteurs

Disons que vous avez le réducteur partiel A et avec un état initial de A(undefined, {}) === Sa et un réducteur B avec un état initial de B(undefined, {}) === { a1: Sa, a2: Sa } où les clés a1 et a2 sont des instances de A .

Une action avec un espace de noms de ['a1'] (* les espaces de noms sont toujours des tableaux ordonnés de chaînes qui ressemblent à la clé de l'état du réducteur partiel) lancée sur B produira le résultat suivant

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

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

Et le contre-exemple d'une action sans espace de noms

const action = {
  type: UNIQUE_ID
};


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

Mises en garde

  • Si A ne gère pas l'action donnée (elle épuise le réducteur), elle doit retourner le même état, cela signifie que pour une action p impossible à gérer pour le réducteur A le résultat de B(undefined, p) devrait être { a1: Sa, a2: Sa } qui est d'ailleurs le même que l'état initial pour B
  • L'action transmise aux réducteurs partiels (notée action* ci-dessus) doit être supprimée de l'espace de noms utilisé pour réduire la portée. Donc, si l'action transmise à B était { type: UNIQUE_ID, namespace: ['a1'] } alors l'action transmise à A est { type: UNIQUE_ID, namespace: [] }
  • Afin d'atteindre cette structure, tous les réducteurs du magasin doivent gérer les actions d'espacement de noms, si ce point est rempli, il n'y a pas de limite sur le nombre d'espaces de noms que vous utilisez et le nombre de réducteurs partiels que vous pouvez imbriquer

Pour y parvenir, j'ai trouvé un pseudo-code pour gérer les espaces de noms dans votre réducteur. Pour que cela fonctionne, nous devons savoir à l'avance si un réducteur peut gérer une action et la quantité de réducteurs partiels qui existent dans le réducteur.

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

C'est à vous de décider si le réducteur peut ou non gérer l'action à l'avance, j'utilise une carte d'objets dans laquelle les types d'action sont les clés et les fonctions de gestionnaire sont les valeurs.

Je suis peut-être un peu en retard pour le jeu, mais j'ai écrit quelques réducteurs génériques qui pourraient aider:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1

Je pense que mutilreducer est une excellente implémentation

@jeffhtli multireducer n'est pas une bonne solution car il n'autorise pas une quantité indéfinie de réducteurs, mais il vous demande de manière préventive de créer une liste de réducteurs statique
Au lieu de cela, j'ai créé un petit projet pour résoudre ce problème en utilisant des UUID pour chaque instance de composants et un état unique pour chaque UUID
https://github.com/eloytoro/react-redux-uuid

Cette page vous a été utile?
0 / 5 - 0 notes