Redux: Como criar uma lista genérica como redutor e aprimorador de componentes?

Criado em 30 set. 2015  ·  50Comentários  ·  Fonte: reduxjs/redux

Olá. Qual seria uma boa maneira de estender o exemplo do contador para uma lista dinâmica de contadores independentes?

Por dinâmico quero dizer que na interface do usuário no final da lista de contadores haveria botões + e - para adicionar um novo contador à lista ou remover o último.

O ideal é que o contra-redutor e o componente permaneçam como estão. Como alguém criaria um armazenamento de lista generalizada + componente para coletar qualquer tipo de entidade? Seria possível generalizar ainda mais a lista store+component para obter contadores e itens de todo do todomvc-example ?

Seria ótimo ter algo assim nos exemplos.

examples

Comentários muito úteis

Como alguém criaria um armazenamento de lista generalizada + componente para coletar qualquer tipo de entidade? Seria possível generalizar ainda mais a lista store+component para obter contadores e itens de todo do exemplo todomvc?

Sim, definitivamente possível.
Você gostaria de criar um redutor de ordem superior e um componente de ordem superior.

Para redutor de ordem superior, veja a abordagem aqui:

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

(Eu não o executei, mas deve funcionar com alterações mínimas.)

Todos 50 comentários

Concordo que este é um bom exemplo.
O Redux é semelhante aqui ao Elm Architecture , então sinta-se à vontade para se inspirar nos exemplos de lá.

Como alguém criaria um armazenamento de lista generalizada + componente para coletar qualquer tipo de entidade? Seria possível generalizar ainda mais a lista store+component para obter contadores e itens de todo do exemplo todomvc?

Sim, definitivamente possível.
Você gostaria de criar um redutor de ordem superior e um componente de ordem superior.

Para redutor de ordem superior, veja a abordagem aqui:

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

(Eu não o executei, mas deve funcionar com alterações mínimas.)

Obrigado - vou tentar fazer essa abordagem funcionar.

Ainda estou querendo saber se posso reutilizar a funcionalidade de lista por meio de combineReducers para ter uma lista de contadores e uma lista de itens de tarefas. E se isso faria sentido. Mas com certeza vou experimentar.

Ainda estou me perguntando se posso reutilizar a funcionalidade de lista por meio de combineReducers para ter uma lista de contadores e uma lista de itens de tarefas. E se isso faria sentido. Mas com certeza vou experimentar.

Sim, totalmente:

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

@gaearon como a ação da lista deve obter seu index ?

Conseguimos criar um redutor de ordem superior com suas instruções, mas estamos lutando com o componente de ordem superior. Atualmente nosso componente não é genérico o suficiente para ser usado com outros componentes além do Counter. Nosso problema é como adicionar o índice às ações de forma genérica.

Você pode ver nossa solução aqui: https://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c

Eu adicionei um comentário ao commit para destacar a parte problemática.

Seria ótimo ter um redutor de lista geral + componente que pudesse fazer uma lista de listas de contadores.

Atualmente nosso componente não é genérico o suficiente para ser usado com outros componentes além do Counter. Nosso problema é como adicionar o índice às ações de forma genérica.

Você pode explicar o que você quer dizer com “adicionar o índice de maneira genérica”? Você quer dizer que deseja ter nomes diferentes para a chave index na ação?

Acho que entendi o que você quis dizer agora.

Desculpe não poder comentar muito agora. Volto amanhã.

Eu entendo o problema agora. Olhando para ele.

Rapidamente me deparei com algumas limitações inerentes a como o Redux se desvia da arquitetura Elm.
É uma pena que eu não os tenha entendido antes!

  • Quando o componente é encapsulado, ele precisa aceitar uma prop dispatch e não retornos de chamada. Isso significa que você precisa evitar fazer dos criadores de ação seus adereços e apenas usar dispatch() nos componentes compostos, ou precisamos introduzir um bindActionCreators(Component, actionCreators) => Component que seja como connect() mas na verdade não se conecta à loja, apenas substituindo action-creator-as-props-wanting-component por this.props.dispatch -wanting-component.
  • Você não pode despachar ações que requerem middleware de componentes encapsulados. Isso é uma chatice! Se eu envolver o contador com uma lista, não posso mais despachar incrementAsync() porque function (dispatch, getState) { ... } que acabei de despachar é transformado em { action: function (dispatch, getState) { ... } } pela lista - e bam! o middleware de conversão não o reconhece mais.

Pode haver soluções não embaraçosas para isso, mas ainda não as vejo.
Por enquanto, veja este commit como um exemplo (com as limitações descritas acima).

Aqui está o código:

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

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

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

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

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

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

contêineres/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 — aqui está um exemplo de design de middleware atual + React Redux quebrando um pouco.
Podemos declarar isso como não será corrigido, mas talvez você queira dar uma olhada se há alguma maneira de evitar isso.

Eu tenho experimentado o uso de um contêiner de serviço (IoC) para React e criei este test-repo ontem: https://github.com/magnusjt/react-ioc

Eu acho que isso poderia resolver parte do problema, já que você pode passar um criador de ação para o Counter sem que o CounterList saiba disso. Isso é possível porque o criador da ação vai no construtor para Counter, não nos adereços.

Para cada novo componente Counter que você cria, você pode passar um criador de ação diferente (talvez vinculando um valor de índice ao criador da ação). Claro que você ainda tem o problema de levar os dados para o contador. Ainda não tenho certeza se isso é algo que pode ser resolvido com um contêiner de serviço.

@gaearon , seu exemplo parece certo para mim. Você tem que passar pelos criadores de ação e despachar todo o caminho. Dessa forma, você pode altar ações com funções de alta ordem.

Eu não estou tão certo de que seu segundo ponto é necessário embora. Você sentirá falta do middleware por causa do novo formato de mensagem, mas um problema maior com performInList é que você limitou a abstração a apenas uma lista. @pe3 mencionou uma lista de listas de contadores. Para abstrações arbitrárias como essa, acho que você precisará aninhar ações de alguma forma.

Neste ticket, crio uma maneira de aninhar os tipos:
https://github.com/rackt/redux/issues/897

Mas acho que mais fundamentalmente, você desejará aninhar as ações inteiramente ....

Ok, acabei de tentar.

Simplifiquei bastante as coisas. Aqui está o aplicativo do contador antes de fazer essas coisas extravagantes:

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

E aqui está depois:

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

Não tenho certeza se "lift" é o termo adequado - eu sei que significa algo na programação funcional, mas me senti bem.

Basicamente, ao levantar uma ação, você está aninhando uma ação dentro de outra.

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

E a ação aninhada é eliminada levantando o redutor. O redutor de elevação basicamente aplica o sub-redutor (que é aplicado parcialmente com a ação apropriada) em algum subestado.

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

Portanto, para o redutor da lista de componentes, tenho uma ação que especifica qual componente em qual índice a subação se aplica.

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

E eu tenho um redutor de "alta ordem" (outro termo chique que parecia certo, haha ​​;) que aplica o sub-redutor ao subestado apropriado.

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

E tudo o que resta é "levantar" o redutor de contagem para o redutor de lista.

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

E agora, para a lista de contadores, só precisamos suspender as ações à medida que as passamos para os contadores.

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

Eu acho que isso poderia ser mais formalizado com linguagem adequada. Acho que as lentes também podem ser usadas aqui para os redutores de alta ordem, mas nunca as usei com sucesso, haha.

E retiro o que disse no último comentário - @gaearon está certo. Aninhando a ação assim, você perderá o middleware e terá que passar o dispatch até o fim para poder manipular os criadores da ação. Talvez para dar suporte a isso, o Redux terá que aplicar todas as sub-ações através do middleware. Além disso, outro problema é inicializar o estado dentro da lista ...

O que você está descrevendo é conhecido como Elm Architecture. Por favor, veja aqui: https://github.com/gaearon/react-elmish-example

Cara, você está sempre um passo à frente! Encha-me com links para coisas legais :+1:

Você não pode despachar ações que requerem middleware de componentes encapsulados. Isso é uma chatice! Se eu envolver o contador com uma lista, não posso mais despachar incrementAsync() porque function (dispatch, getState) { ... } que acabei de despachar é transformado em { action: function (dispatch, getState) { ... } } por a lista — e bam! o middleware de conversão não o reconhece mais.

@gaearon que tal esta solução? em vez do redutor genérico chamando a si mesmo de redutor filho assim

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

fornecer à loja algum método especial dispatchTo(reducer, state, action, callback) que age como dispatch , exceto que despacha diretamente para um redutor filho (através de todos os middelwares configurados) e notifica o retorno de chamada em cada modificação de estado filho

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

Não sei se isso é possível no Redux. Uma idéia é implementar dispatchTo usando algum método interno store.derive(reducer, state) que retornaria um armazenamento filho para aquela parte da árvore de estado configurada com alguns middlewares como o armazenamento raiz. por exemplo

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

Esta é apenas uma ideia, como eu disse, não estou ciente dos componentes internos do Redux, então talvez eu tenha perdido alguma coisa

EDITAR
_provavelmente isso será estranho, pois os métodos redutores devem ser síncronos. tornar um método assíncrono implica que todo o chaine up também deve ser assíncrono.
Talvez a melhor solução seja expor diretamente o método store.derive(reducer) e construir redutores genéricos usando algum tipo de composição da Loja_

Isso é muita complicação e não vale a pena IMO. Se você quiser fazer dessa maneira, apenas não use o middleware (ou use alguma implementação alternativa de applyMiddleware ) e está tudo pronto.

Além disso, estou fechando porque não planejamos agir sobre isso.

@gaearon para fins de discussão:
o exemplo de código que você anexou neste commit: https://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4

Caso eu tenha 2 listas de contadores (ou mesmo 'models' separados), despachar addToList adicionaria item a ambas as listas, pois os tipos de ação são os mesmos.

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

então como o reducers/list ajuda aqui? Você não precisa prefixar tipos de ação ou algo assim?

Caso eu tenha 2 listas de contadores (ou mesmo 'models' separados), despachar addToList adicionaria item a ambas as listas, porque os tipos de ação são os mesmos.

Por favor, dê uma olhada em a83002aed8e36f901ebb5f139dd14ce9c2e4cab4. Aninha ações. dispatch que é passado do componente de contêiner envolve ações em ação performInList . Em seguida, a ação interna é recuperada no redutor. É basicamente assim que a Elm Architecture funciona.

@gaearon talvez esteja faltando alguma coisa, mas junto com o redutor extra counterList2 mencionado acima, essa interface do usuário ainda atualiza as duas listas em cada ação (o que é esperado de acordo com a forma como é construído, mas qual é a solução?):

// 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 você precisará envolvê-lo em uma lista novamente para que as ações não entrem em conflito para as duas listas, da mesma forma que fizemos com a lista de contadores

@ccorcos

para envolvê-lo em uma lista novamente

embrulhar o que exatamente?

@ccorcos
Eu carreguei o exemplo aqui: http://elado.s3-website-us-west-1.amazonaws.com/redux-counter/
Tem mapas de origem

Ainda não tenho certeza do que você quis dizer. Como eu disse - o comportamento atual é o esperado porque os nomes das ações são os mesmos e não há indicação no criador da ação em qual lista executá-la, então ele executa a ação em todos os redutores que eventualmente afetam as duas listas.

Portanto, não conheço muito bem as funções reais do Redux, mas estou muito familiarizado com o elm.

Neste novo exemplo, não vinculamos os criadores de ação no nível superior. Em vez disso, passamos a função dispatch para os componentes inferiores e esses componentes inferiores podem passar uma ação para a função dispatch.

Para que a abstração funcione bem e não tenhamos colisões de ação, quando o componente "listOf" passa o despacho para seus filhos, ele passa uma função que envolve a ação em um formato que o componente de lista pode entender.

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

Então agora você pode compor listOf(counter) ou listOf(listOf(counter)) e se você quiser criar um componente chamado pairOf , então você precisa ter certeza de encapsular as ações quando você as passar. No momento, o componente App apenas os renderiza lado a lado sem envolver as funções de despacho, portanto, você tem colisões de ação.

@ccorcos

Obrigado. Então, para concluir, obviamente não há "mágica" acontecendo, as ações precisam de todas as informações necessárias para informar ao redutor em qual instância executar a ação. Se for um listOf(listOf(counter)) a ação precisará dos dois índices do array 2d. listOf(listOf(counter)) pode funcionar, mas ter todos os contadores em uma única lista simples indexada por ID exclusivo, que é a única coisa passada em uma ação, parece mais flexível.

Parece que a única maneira de construir um aplicativo Redux flexível e complexo é ter todas as entidades no sistema indexadas por ID na loja. Qualquer outra abordagem mais simples, que às vezes é dada em exemplos, atingirá seus limites rapidamente. Esse índice é quase um espelho de um banco de dados relacional.

a ação precisará de ambos os índices da matriz 2d

parece que você está pensando e a ação se parece com:

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

mas deve realmente se parecer com:

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

Dessa forma, você pode fazer um listOf(listOf(listOf(...listOf(counter)...))) para sempre!

Isso tudo vem de Elm, btw. Confira o tutorial de arquitetura Elm

Estou um pouco lento para a festa, mas não vejo onde isso "não funciona com middleware"? Se você está oferecendo aninhamento infinito como @ccorcos , você não pode simplesmente conectar o middleware para lidar com o aninhamento? Ou estamos falando exclusivamente de redux-thunk onde ações aninhadas significariam estranheza?

Como o middleware saberia se deve interpretar a ação como está ou procurar por ações aninhadas?

Ah.

@gaearon

Oi.

Não tenho certeza de ter entendido a resolução sobre o problema e realmente aprecio uma resposta simples.

É _não usar middleware (e basicamente a maior parte do ecossistema Redux), se você tiver várias cópias do componente na mesma página_? Também não vi se alguém respondeu à sugestão do multiredutor .

Qualquer esclarecimento ajudaria.

Não, esta não é a resolução. A discussão não é sobre ter várias identidades de um componente na página. Para implementar isso, basta passar um ID na ação. O tópico era sobre _escrever uma função genérica para fazer isso_. O que infelizmente colide com o conceito de middleware.

Obrigada.

Olá pessoal,
Talvez eu tenha resolvido esse problema, porém prefiro usar deku em vez react ^^

Eu ficaria feliz se alguém me desse sua visão sobre este experimento, especialmente sobre a ideia taskMiddleware . @gaearon você tem algum tempo para conferir? :língua:

Lista de contadores

Obrigado!

Só quero esclarecer que nenhuma dessas soluções visa lidar com um estado inicial.
Isso poderia ser alcançado de alguma forma @gaearon ? Permitindo que o redutor de lista acima tenha uma lista inicial de contadores?

Por que isso seria problemático? Imagino que você possa chamar os redutores filhos com estado undefined e usar esses valores.

Quando a loja é inicializada com o primeiro despacho eu preciso (para minha lógica interna) ter um estado inicial para essa lista, e deve ser uma lista predefinida de contadores (o tipo de itens da lista)

Algo assim?

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

Você pode fazer disso um argumento para list também se quiser

Isso parece realmente complicado apenas para que eu possa ter vários do mesmo componente em uma página.

Ainda estou pensando no Redux, mas criar várias lojas parece muito mais simples, mesmo que não seja um padrão de uso do Redux recomendado.

@deevus : não deixe que algumas dessas discussões o assustem. Há várias pessoas na comunidade Redux que são muito orientadas para a Programação Funcional e, embora alguns dos conceitos discutidos neste tópico e outros semelhantes tenham valor, eles também tendem a ser uma tentativa de buscar o "perfeito" , em vez do meramente "bom".

Você pode ter várias instâncias de um componente em uma página, em geral. O que esta discussão visa é a composição arbitrária de componentes aninhados, o que é interessante, mas também não é algo que a maioria dos aplicativos precisará fazer.

Se você tiver preocupações específicas além disso, o Stack Overflow geralmente é um bom lugar para fazer perguntas. Além disso, a comunidade Reactiflux no Discord tem vários canais de bate-papo dedicados a discutir o React e tecnologias relacionadas, e sempre há algumas pessoas dispostas a conversar e ajudar.

@markerikson Obrigado. Vou tentar obter alguma ajuda do Reactiflux no Discord.

Acompanhando isso:
Encontrei uma maneira escalável de reutilizar redutores em meu projeto, até mesmo reutilizar redutores dentro de redutores.

O paradigma do namespace

É o conceito de que as ações que agem sobre um dos muitos redutores "parciais" possuem uma propriedade "namespace" que determina qual redutor manipulará a ação ao contrário de _todos_ os redutores manipulando-a porque ouvem a mesma ação type (exemplo em https://github.com/reactjs/redux/issues/822#issuecomment-172958967)

No entanto, as ações que não contêm um namespace ainda serão propagadas para todos os redutores parciais dentro de outros redutores

Digamos que você tenha o redutor parcial A e com um estado inicial de A(undefined, {}) === Sa e um redutor B com um estado inicial de B(undefined, {}) === { a1: Sa, a2: Sa } onde as chaves a1 e a2 são instâncias de A .

Uma ação com namespace de ['a1'] (* namespaces são sempre arrays ordenados de strings que lembram a chave do estado para o redutor parcial) lançado em B produzirá o seguinte resultado

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

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

E o contra-exemplo de uma ação sem namespace

const action = {
  type: UNIQUE_ID
};


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

Ressalvas

  • Se A não lidar com a ação dada (exaustou o redutor) ele deve retornar o mesmo estado, isso significa que para uma ação p que não é manipulável para o redutor A o resultado de B(undefined, p) deve ser { a1: Sa, a2: Sa } que é o mesmo que o estado inicial para B
  • A ação passada para os redutores parciais (indicada como action* acima) deve ser removida do namespace usado para restringir o escopo. Então, se a ação passada para B foi { type: UNIQUE_ID, namespace: ['a1'] } , então a ação passada para A é { type: UNIQUE_ID, namespace: [] }
  • Para alcançar essa estrutura, todos os redutores na loja devem lidar com ações de namespace, se esse ponto for atendido, não há limite de quantos namespaces você está usando e quantos redutores parciais você pode aninhar

Para conseguir isso, criei um pseudocódigo para lidar com namespaces em seu redutor. Para que isso funcione, devemos saber de antemão se um redutor pode manipular uma ação e a quantidade de redutores parciais que existem no redutor.

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

Cabe a você decidir se o redutor pode ou não manipular a ação de antemão, eu uso um mapa de objetos no qual os tipos de ação são as chaves e as funções do manipulador são os valores.

Eu posso estar um pouco atrasado para o jogo, mas escrevi alguns redutores de propósito genérico que podem ajudar:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1

Eu acho que o mutilreducer é uma ótima implementação

@jeffhtli multireducer não é uma boa solução porque não permite uma quantidade indefinida de redutores, em vez disso, solicita preventivamente que você construa uma lista estática de redutores
Em vez disso, criei um pequeno projeto para resolver esse problema usando UUIDs para cada instância de componentes e um estado exclusivo para cada UUID
https://github.com/eloytoro/react-redux-uuid

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

CellOcean picture CellOcean  ·  3Comentários

vslinko picture vslinko  ·  3Comentários

captbaritone picture captbaritone  ·  3Comentários

caojinli picture caojinli  ·  3Comentários

vraa picture vraa  ·  3Comentários