Привет. Что было бы хорошим способом расширить пример счетчика до динамического списка независимых счетчиков?
Под динамическим я подразумеваю, что в UI в конце списка счетчиков будут кнопки +
и -
для добавления нового счетчика в список или удаления последнего.
В идеале редуктор счетчика и компонент должны оставаться такими, какие они есть. Как можно создать обобщенное хранилище списков + компонент для сбора любых сущностей? Можно ли еще больше обобщить хранилище списка + компонент, чтобы взять как счетчики, так и элементы задач из примера todomvc ?
Было бы здорово иметь что-то подобное в примерах.
Я согласен, что это хороший пример.
Redux здесь похож на Elm Architecture , поэтому не стесняйтесь черпать вдохновение из примеров.
Как можно создать обобщенное хранилище списков + компонент для сбора любых сущностей? Можно ли еще больше обобщить хранилище списка + компонент, чтобы взять как счетчики, так и элементы задач из примера todomvc?
Да, определенно возможно.
Вы хотели бы создать редуктор более высокого порядка и компонент более высокого порядка.
Для редуктора более высокого порядка см. подход здесь:
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
});
(Я не запускал его, но он должен работать с минимальными изменениями.)
Спасибо - я постараюсь заставить этот подход работать.
Мне все еще интересно, могу ли я повторно использовать функциональность списка через combineReducers
, чтобы иметь как список счетчиков, так и список задач. И если бы в этом был смысл. Но я обязательно попробую.
Мне все еще интересно, могу ли я повторно использовать функциональность списка через combReducers, чтобы иметь как список счетчиков, так и список элементов todo. И если бы в этом был смысл. Но я обязательно попробую.
Да, полностью:
const reducer = combineReducers({
counterList: list(counter, {
add: 'ADD_COUNTER',
remove: 'REMOVE_COUNTER'
}),
todoList: list(counter, {
add: 'ADD_TODO',
remove: 'REMOVE_TODO'
}),
});
@gaearon , как действие списка должно получить index
?
Нам удалось создать редуктор более высокого порядка с вашими инструкциями, но мы боремся с компонентом более высокого порядка. В настоящее время наш компонент недостаточно универсален, чтобы его можно было использовать с другими компонентами, кроме Counter. Наша проблема заключается в том, как добавить индекс к действиям в общем виде.
Вы можете увидеть наше решение здесь: https://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c .
Я добавил комментарий к коммиту, чтобы выделить проблемную часть.
Было бы здорово иметь общий редуктор списка + компонент, который может составлять списки списков счетчиков.
В настоящее время наш компонент недостаточно универсален, чтобы его можно было использовать с другими компонентами, кроме Counter. Наша проблема заключается в том, как добавить индекс к действиям в общем виде.
Можете ли вы объяснить, что вы подразумеваете под «добавлением индекса в общем виде»? Вы имеете в виду, что хотите иметь разные имена для клавиши index
в действии?
Думаю, теперь я понимаю, что вы имеете в виду.
Извините, я не могу сейчас много комментировать. Я вернусь завтра.
Теперь я понимаю проблему. Глядя на это.
Я быстро столкнулся с некоторыми ограничениями, присущими тому, как Redux отличается от архитектуры Elm.
Жаль, что я не понимал их раньше!
dispatch
, а не обратные вызовы. Это означает, что вам нужно либо не делать создателей действий своим реквизитом и просто использовать dispatch()
в составных компонентах, либо нам нужно ввести bindActionCreators(Component, actionCreators) => Component
, который аналогичен connect()
но на самом деле не подключается к хранилищу, а просто заменяет action-creator-as-props-wanting-component на this.props.dispatch
-wanting-component.incrementAsync()
, потому что только что отправленные function (dispatch, getState) { ... }
превращаются списком в { action: function (dispatch, getState) { ... } }
— и бам! промежуточное ПО thunk больше не распознает его.Могут быть не неудобные решения для этого, но я их пока не вижу.
А пока посмотрите этот коммит в качестве примера (с ограничениями, описанными выше).
Вот код:
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;
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>
)
}
}
};
}
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);
};
}
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
};
}
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;
}
}
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;
}
}
}
import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'
const counterList = list(counter);
const rootReducer = combineReducers({
counterList
});
export default rootReducer;
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 — вот пример того, как текущее промежуточное ПО + дизайн React Redux несколько ломаются.
Мы можем объявить это как wontfix, но, может быть, вы захотите посмотреть, есть ли способ обойти это.
Было бы
https://github.com/erikras/multireducer/ помочь?
Я экспериментировал с использованием контейнера службы (IoC) для React и вчера создал этот тестовый репозиторий: https://github.com/magnusjt/react-ioc .
Я думаю, что это потенциально может решить часть проблемы, поскольку вы можете передать создателя действия счетчику без ведома CounterList. Это возможно, потому что создатель действия находится в конструкторе Counter, а не в свойствах.
Для каждого нового компонента счетчика, который вы создаете, вы можете передать другого создателя действия (возможно, привязав значение индекса к создателю действия). Конечно, у вас все еще есть проблема с передачей данных на счетчик. Я еще не уверен, что это можно решить с помощью сервисного контейнера.
@gaearon , твой пример мне кажется правильным. Вы должны пройти создателей действий и отправить их вниз. Таким образом, вы можете чередовать действия с функциями высокого порядка.
Я не уверен, что ваш второй пункт необходим. Вам будет не хватать промежуточного программного обеспечения из-за нового формата сообщений, но более серьезная проблема с performInList
заключается в том, что вы ограничили абстракцию только одним списком вверх. @pe3 упомянул список списков счетчиков. Я думаю, что для такой произвольной абстракции вам нужно как-то вложить действия.
В этом тикете я придумываю способ вложения типов:
https://github.com/rackt/redux/issues/897
Но я думаю более фундаментально, вы захотите полностью вложить действия....
Хорошо, я только что попробовал.
Я немного упростил ситуацию. Вот приложение счетчика, прежде чем делать эти причудливые вещи:
А вот после:
Я не уверен, что «подъем» является правильным термином — я знаю, что это что-то значит в функциональном программировании, но мне было нормально.
По сути, поднимая действие, вы вкладываете действие в другое.
const liftActionCreator = liftingAction => actionCreator => action => Object.assign({}, liftingAction, { nextAction: actionCreator(action) })
И вложенное действие удаляется при поднятии редуктора. Редуктор подъема в основном применяет субредуктор (который частично применяется с соответствующим действием) к некоторому подсостоянию.
const liftReducer = liftingReducer => reducer => (state, action) => liftingReducer(state, action)((subState) => reducer(subState, action.nextAction))
Итак, для редуктора списка компонентов у меня есть действие, которое указывает, к какому компоненту и к какому индексу применяется поддействие.
// list actions
const LIST_INDEX = 'LIST_INDEX'
function actOnIndex(i) {
return {
type: LIST_INDEX,
index: i
}
}
И у меня есть редуктор «высокого порядка» (еще один причудливый термин, который кажется правильным, ха-ха;) редуктор, который применяет субредуктор к соответствующему подсостоянию.
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;
}
}
И все, что осталось, это «поднять» редьюсер количества в редуктор списка.
const reducer = combineReducers({
counts: liftReducer(list)(count)
});
А теперь для списка счетчиков нам просто нужно поднимать действия, когда мы передаем их счетчикам.
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>
);
}
}
Я думаю, что это могло бы быть более формализовано с правильным жаргоном. Я думаю, что линзы можно было бы использовать здесь и для редукторов высокого порядка, но я никогда не использовал их успешно, ха-ха.
И я беру назад то, что сказал в последнем комментарии — @gaearon прав. Вкладывая действие таким образом, вы пропустите промежуточное программное обеспечение, и вам придется полностью передать диспетчеризацию, чтобы вы могли манипулировать создателями действий. Возможно, для поддержки этого Redux придется применять все поддействия через промежуточное ПО. Кроме того, другой проблемой является инициализация состояния в списке...
То, что вы описываете, известно как Elm Architecture. Пожалуйста, смотрите здесь: https://github.com/gaearon/react-elmish-example
Чувак, ты всегда на шаг впереди! Осыпайте меня ссылками на интересные вещи :+1:
Вы не можете отправлять действия, требующие промежуточного программного обеспечения, из обернутых компонентов. Это облом! Если я оберну счетчик списком, я больше не смогу отправлять incrementAsync(), потому что функция (dispatch, getState) { ... }, которую я только что отправил, превращается в { action: function (dispatch, getState) { ... } } на список — и бац! промежуточное ПО thunk больше не распознает его.
@gaearon как насчет этого решения? вместо того, чтобы общий редуктор называл себя дочерним редуктором, как это
case PERFORM_IN_LIST:
return [
...state.slice(0, index),
reducer(state[index], innerAction),
...state.slice(index + 1)
];
предоставьте хранилищу некоторый специальный метод dispatchTo(reducer, state, action, callback)
, который действует как dispatch
, за исключением того, что он отправляет непосредственно дочернему редюсеру (через все настроенные промежуточные программы) и уведомляет об обратном вызове при каждой модификации дочернего состояния.
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;
}
}
}
Я не знаю, возможно ли это в Redux. Идея состоит в том, чтобы реализовать dispatchTo
с помощью некоторого внутреннего метода store.derive(reducer, state)
, который будет возвращать дочернее хранилище для той части дерева состояний, настроенной с некоторыми промежуточными программами в качестве корневого хранилища. Например
function dispatchTo(reducer, state, action, callback) {
const childStore = store.derive(reducer, state)
childStore.subscribe(() => setRootState( callback(getState() ))
childStore.dispatch(action)
}
Это просто идея, как я сказал, я не знаю о внутренностях Redux, поэтому, возможно, я что-то пропустил.
РЕДАКТИРОВАТЬ
_вероятно, это будет неудобно, поскольку методы редуктора должны быть синхронными. создание асинхронного метода подразумевает, что вся цепочка вверх также должна быть асинхронной.
Возможно, лучшее решение — напрямую использовать метод store.derive(reducer)
и создавать общие редукторы, используя какую-то композицию Store_
Это слишком сложно и не стоит ИМО. Если вы хотите сделать это таким образом, просто не используйте промежуточное программное обеспечение (или используйте какую-либо альтернативную реализацию applyMiddleware
), и все готово.
Кроме того, я закрываю, потому что мы не планируем действовать по этому поводу.
@gaearon ради обсуждения:
пример кода, который вы приложили к этой фиксации: https://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4
В случае, если у меня есть 2 списка счетчиков (или даже отдельные «модели»), отправка addToList
добавит элемент в оба списка, потому что типы действий одинаковы.
// 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;
так как здесь поможет reducers/list
? Разве вам не нужно префикс типов действий или что-то в этом роде?
В случае, если у меня есть 2 списка счетчиков (или даже отдельные «модели»), диспетчеризация addToList добавит элемент в оба списка, потому что типы действий одинаковы.
Пожалуйста, внимательно посмотрите на a83002aed8e36f901ebb5f139dd14ce9c2e4cab4. В него вложены действия. dispatch
, который передается из компонента-контейнера, оборачивает действия в действие performInList
. Затем внутреннее действие извлекается в редьюсере. Примерно так работает Elm Architecture.
@gaearon , возможно, я что-то упускаю, но вместе с упомянутым выше дополнительным редуктором counterList2
этот пользовательский интерфейс по-прежнему обновляет оба списка при каждом действии (что ожидается в зависимости от того, как оно построено, но каково решение?):
// 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 вам нужно снова обернуть его в список, чтобы действия не конфликтовали для двух списков, точно так же, как мы сделали со списком счетчиков.
@ccorcos
чтобы снова обернуть его в список
завернуть что именно?
@ccorcos
Я загрузил пример здесь: http://elado.s3-website-us-west-1.amazonaws.com/redux-counter/
Он имеет исходные карты
Все еще не совсем уверен, что вы имели в виду. Как я уже сказал, текущее поведение является ожидаемым, потому что имена действий одинаковы, и в создателе действия нет указаний, в каком списке его выполнять, поэтому он запускает действие для всех редукторов, что в конечном итоге влияет на оба списка.
Так что я не очень хорошо знаю настоящие функции Redux, но я очень хорошо знаком с elm.
В этом новом примере мы не связываем создателей действий на верхнем уровне. Вместо этого мы передаем функцию отправки нижестоящим компонентам, и эти компоненты могут передавать действие функции отправки.
Чтобы абстракция работала хорошо, чтобы у нас не было конфликтов действий, когда компонент «listOf» передает диспетчеризацию своим дочерним элементам, он фактически передает функцию, которая оборачивает действие в формате, который может понять компонент списка.
children.map((child, i) => {
childDispatch = (action) => dispatch({action, index: i})
// ...
Итак, теперь вы можете составить listOf(counter)
или listOf(listOf(counter))
, и если вы хотите создать компонент с именем pairOf
, вам нужно обязательно обернуть действия, когда вы их передаете. Прямо сейчас компонент App
просто отображает их рядом, не оборачивая функции диспетчеризации, поэтому у вас есть коллизии действий.
@ccorcos
Спасибо. Итак, в заключение, очевидно, что никакого «волшебства» не происходит, действиям нужна вся информация, необходимая им, чтобы сообщить редюсеру, над каким экземпляром выполнять действие. Если это listOf(listOf(counter))
, для действия потребуются оба индекса массива 2d. listOf(listOf(counter))
может работать, но наличие всех счетчиков в одном плоском списке, проиндексированном уникальным идентификатором, который является единственным, что передается в действии, кажется более гибким.
Похоже, что единственный способ создать гибкое и сложное приложение Redux — это проиндексировать все объекты в системе по идентификатору в магазине. Любой другой более простой подход, который иногда приводится в примерах, быстро достигает своих пределов. Этот индекс является почти зеркалом реляционной БД.
для действия потребуются оба индекса массива 2d
это звучит так, как будто вы думаете, а действие выглядит так:
{type: 'increment', index:[0 5]}
но на самом деле это должно выглядеть так:
{type:'child', index: 0, action: {type: 'child', index: 5, action: {type: 'increment'}}}
Таким образом, вы можете делать listOf(listOf(listOf(...listOf(counter)...)))
навсегда!
Это все исходит от Elm, кстати. Ознакомьтесь с учебным пособием по архитектуре Elm
Я немного медлителен с вечеринкой, но я не вижу, где это «не работает с промежуточным программным обеспечением»? Если вы предлагаете бесконечную вложенность как @ccorcos , не можете ли вы просто подключить промежуточное программное обеспечение для обработки вложенности? Или мы говорим исключительно о redux-thunk
, где вложенные действия означают странность?
Как промежуточное ПО узнает, следует ли интерпретировать действие как есть или искать вложенные действия?
Ага.
@gaearon
Привет.
Я не уверен, что понял решение по этому вопросу и был бы очень признателен за простой ответ.
Это _не использовать промежуточное ПО (и, в основном, большую часть экосистемы Redux), если у вас есть несколько копий компонента на одной странице_? Я также не видел, ответил ли кто-нибудь на предложение мультиредуктора .
Любое разъяснение поможет.
Нет, это не разрешение. Поток не связан с наличием нескольких идентификаторов компонента на странице. Чтобы реализовать это, вы можете просто передать идентификатор в действии. Тема была посвящена написанию универсальной функции для этого. Что, к сожалению, противоречит концепции промежуточного программного обеспечения.
Спасибо.
Всем привет,
Возможно, я решил эту проблему, однако я предпочитаю использовать deku
вместо react
^^
Я был бы счастлив, если бы кто-нибудь рассказал мне об этом эксперименте, особенно об идее taskMiddleware
. @gaearon , у тебя есть время, чтобы проверить это? :язык:
Спасибо!
Просто хочу уточнить, что ни одно из этих решений не предназначено для обработки начального состояния.
Можно ли как-то добиться этого @gaearon ? Разрешить приведенному выше редуктору списка иметь начальный список счетчиков?
Почему это будет проблематично? Я предполагаю, что вы могли бы вызвать дочерние редукторы с состоянием undefined
и использовать эти значения.
Когда хранилище инициализируется с первой отправкой, мне нужно (для моей внутренней логики) иметь начальное состояние для этого списка, и это должен быть предопределенный список счетчиков (тип элементов списка)
Что-то вроде этого?
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;
// ...
}
}
Вы также можете сделать это аргументом для list
, если хотите
Это кажется очень сложным, потому что я могу иметь несколько одинаковых компонентов на странице.
Я все еще ломаю голову над Redux, но создание нескольких хранилищ кажется намного проще, даже если это не рекомендуемый шаблон использования Redux.
@deevus : пусть вас не пугают некоторые из этих дискуссий. В сообществе Redux есть ряд людей, которые очень ориентированы на функциональное программирование, и хотя некоторые из концепций, обсуждаемых в этой и других подобных ветках, имеют ценность, они также, как правило, являются чем-то вроде попытки достичь «идеального» , а не просто "хорошо".
В общем, вы можете иметь несколько экземпляров компонента на странице. Это обсуждение направлено на произвольную композицию вложенных компонентов, что интересно, но не то, что нужно делать большинству приложений.
Если у вас есть особые проблемы помимо этого, Stack Overflow обычно является хорошим местом, чтобы задать вопрос. Кроме того, в сообществе Reactiflux на Discord есть несколько чат-каналов, посвященных обсуждению React и связанных с ним технологий, и всегда есть люди, желающие поговорить и помочь.
@markerikson Спасибо. Я постараюсь получить помощь от Reactiflux в Discord.
Следуя этому:
Я нашел масштабируемый способ повторного использования редьюсеров в своем проекте, даже повторного использования редьюсеров внутри редюсеров.
Парадигма пространства имен
Его концепция заключается в том, что действия, которые действуют на один из многих «частичных» редукторов, содержат свойство «пространства имен», которое определяет, какой редуктор будет обрабатывать действие, в отличие от _всех_ редюсеров, обрабатывающих его, потому что они слушают одно и то же действие type
(пример в https://github.com/reactjs/redux/issues/822#issuecomment-172958967)
Однако действия, которые не содержат пространства имен, по-прежнему будут распространяться на все частичные редьюсеры в других редьюсерах.
Скажем, у вас есть частичный редуктор A
с начальным состоянием A(undefined, {}) === Sa
и редьюсер B
с начальным состоянием B(undefined, {}) === { a1: Sa, a2: Sa }
, где ключи a1
и a2
являются экземплярами A
.
Действие с пространством имен ['a1']
(* пространства имен всегда представляют собой упорядоченные массивы строк, которые напоминают ключ состояния к частичному редюсеру), приведенное к B
, приведет к следующему результату.
const action = {
type: UNIQUE_ID,
namespace: ['a1']
};
B(undefined, action) == { a1: A(undefined, action*), a2: Sa }
И встречный пример действия без пространства имен
const action = {
type: UNIQUE_ID
};
B(undefined, action) == { a1: A(undefined, action), a2: A(undefined, action) }
Предостережения
A
не обрабатывает данное действие (исчерпает редуктор), он должен вернуть то же состояние, это означает, что для действия p
, которое невозможно обработать для редуктора A
результат B(undefined, p)
должен быть { a1: Sa, a2: Sa }
, что, кстати, совпадает с начальным состоянием для B
action*
), должно быть лишено пространства имен, используемого для сужения области действия. Таким образом, если действие, переданное в B
$, равно $ { type: UNIQUE_ID, namespace: ['a1'] }
, то действие, переданное в A
, равно { type: UNIQUE_ID, namespace: [] }
Чтобы добиться этого, я придумал некоторый псевдокод для обработки пространств имен в вашем редюсере. Чтобы это работало, мы должны заранее знать, может ли редуктор обрабатывать действие, и количество частичных редюсеров, которые существуют в редьюсере.
(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);
}
Вам решать, как решить, может или не может редуктор обработать действие заранее, я использую карту объектов, в которой типы действий являются ключами, а функции обработчика — значениями.
Возможно, я немного опоздал, но я написал несколько редукторов общего назначения, которые могут помочь:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1
Я думаю, что mutilreducer - отличная реализация
@jeffhtli multireducer не является хорошим решением, потому что он не допускает неопределенного количества редюсеров, вместо этого он предварительно просит вас создать список статических редукторов.
Вместо этого я создал небольшой проект для решения этой проблемы, используя UUID для каждого экземпляра компонентов и уникальное состояние для каждого UUID.
https://github.com/eloytoro/реакт-редукс-uuid
Самый полезный комментарий
Да, определенно возможно.
Вы хотели бы создать редуктор более высокого порядка и компонент более высокого порядка.
Для редуктора более высокого порядка см. подход здесь:
(Я не запускал его, но он должен работать с минимальными изменениями.)