упростить сокращение с помощью общего действия и редуктора
В проекте React-Redux люди обычно создают несколько действий и редукторов для каждого подключенного компонента. Однако это создает много кода для простых обновлений данных.
Хорошей практикой является использование единого родового действия и редуктора для инкапсуляции всех изменений данных, чтобы упростить и закрепить разработку приложений.
Каковы были бы недостатки или потеря производительности при использовании этого метода. Потому что я не вижу существенного компромисса, и это делает разработку намного проще, и мы можем поместить все их в один файл! Пример такой архитектуры:
// Say we're in user.js, User page
// state
var initialState = {};
// generic action --> we only need to write ONE DISPATCHER
function setState(obj){
Store.dispatch({ type: 'SET_USER', data: obj });
}
// generic reducer --> we only need to write ONE ACTION REDUCER
function userReducer = function(state = initialState, action){
switch (action.type) {
case 'SET_USER': return { ...state, ...action.data };
default: return state;
}
};
// define component
var User = React.createClass({
render: function(){
// Here the magic...
// We can just call the generic setState() to update any data.
// No need to create separate dispatchers and reducers,
// thus greatly simplifying and fasten app development.
return [
<div onClick={() => setState({ someField: 1 })}/>,
<div onClick={() => setState({ someOtherField: 2, randomField: 3 })}/>,
<div onClick={() => setState({ orJustAnything: [1,2,3] })}/>
]
}
});
// register component for data update
function mapStateToProps(state){
return { ...state.user };
}
export default connect(mapStateToProps)(User);
редактировать
Таким образом, типичная архитектура Redux предлагает создать:
- Централизованные файлы со всеми действиями
- Централизованные файлы со всеми редукторами
Вопрос в том, почему двухэтапный процесс? Здесь другое архитектурное предложение:
Создайте 1 набор файлов, содержащих все setXField()
которые обрабатывают все изменения данных. И другие компоненты просто используют их для запуска изменений. Легко. Пример:
/** UserAPI.js
* Containing all methods for User.
* Other components can just call them.
*/
// state
var initialState = {};
// generic action
function setState(obj){
Store.dispatch({ type: 'SET_USER', data: obj });
}
// generic reducer
function userReducer = function(state = initialState, action){
switch (action.type) {
case 'SET_USER': return { ...state, ...action.data };
default: return state;
}
};
// API that we export
let UserAPI = {};
// set user name
UserAPI.setName = function(name){
$.post('/user/name', { name }, function({ ajaxSuccess }){
if (ajaxSuccess) setState({ name });
});
};
// set user picture URL
UserAPI.setPicture = function(url){
$.post('/user/picture', { url }, function({ ajaxSuccess }){
if (ajaxSuccess) setState({ url });
});
};
// logout, clear user
UserAPI.logout = function(){
$.post('/logout', {}, function(){
setState(initialState);
});
};
// Etc, you got the idea...
// Moreover, you can add a bunch of other User related methods,
// like some helper methods unrelated to Redux, or Ajax getters.
// Now you have everything related to User available in a single file!
// It becomes much easier to read through and understand.
// Finally, you can export a single UserAPI object, so other
// components only need to import it once.
export default UserAPI
Прочитайте комментарии в разделе выше.
Теперь вместо того, чтобы иметь кучу действий/диспетчеров/редукторов. У вас есть 1 файл, инкапсулирующий все необходимое для концепции пользователя. Почему это плохая практика? IMO, это значительно облегчает жизнь программиста, и другие программисты могут просто читать файл сверху донизу, чтобы понять бизнес-логику, им не нужно переключаться между файлами действий/редукторов. Черт возьми, даже redux-thunk
не нужна! И вы можете даже проверять функции один за другим. Поэтому тестируемость не теряется.
Ответы
Ответ 1
Во-первых, вместо вызова store.dispatch
в вашем создателе действия он должен вернуть объект (действие), что упрощает тестирование и позволяет рендеринг сервера.
const setState = (obj) => ({
type: 'SET_USER',
data: obj
})
onClick={() => this.props.setState(...)}
// bind the action creator to the dispatcher
connect(mapStateToProps, { setState })(User)
Вы также должны использовать класс ES6 вместо React.createClass
.
Вернемся к теме, более специализированный создатель действия будет примерно таким:
const setSomeField = value => ({
type: 'SET_SOME_FIELD',
value,
});
...
case 'SET_SOME_FIELD':
return { ...state, someField: action.value };
Преимущества этого подхода над вашим общим
1. Более высокое повторное использование
Если someField
установлен в нескольких местах, он setSomeField(someValue)
вызов setSomeField(someValue)
чем setState({ someField: someValue })}
.
2. Более высокая проверяемость
Вы можете легко проверить setSomeField
чтобы убедиться, что он правильно изменяет только соответствующее состояние.
С помощью generic setState
вы можете протестировать и для setState({ someField: someValue })}
, но нет прямой гарантии, что весь ваш код будет правильно называть.
Например. кто-то из вашей команды может сделать опечатку и вызвать setState({ someFeild: someValue })}
.
Заключение
Недостатки не так уж и значительны, поэтому прекрасно использовать создателя общих действий для сокращения числа специализированных создателей действий, если вы считаете, что это стоит компромисс для вашего проекта.
РЕДАКТИРОВАТЬ
Что касается вашего предложения по размещению редукторов и действий в одном файле: обычно он предпочитает хранить их в отдельных файлах для модульности; это общий принцип, который не является уникальным для Реагирования.
Однако вы можете поместить связанные редукторы и файлы действий в одну и ту же папку, что может быть лучше/хуже в зависимости от ваших требований к проекту. Смотрите это и это для некоторого фона.
Вам также потребуется экспортировать userReducer
для вашего корневого редуктора, если вы не используете несколько магазинов, которые обычно не рекомендуются.
Ответ 2
В основном я чаще всего использую редукцию для кэширования ответов API, здесь несколько случаев, когда я думал, что он ограничен.
1) Что делать, если я вызываю разные API, которые имеют один и тот же ключ, но идут к другому объекту?
2) Как я могу позаботиться, если данные являются потоком из сокета? Нужно ли мне итерировать объект, чтобы получить тип (поскольку тип будет в заголовке и ответе в полезной нагрузке) или попросить мой серверный ресурс отправить его с помощью определенной схемы.
3) Это также не подходит для api, если мы используем сторонний поставщик, где мы не контролируем вывод, который мы получаем.
Всегда полезно контролировать, какие данные идут туда. В приложениях, которые очень большие, например, приложение для мониторинга сети, мы можем переписать данные, если у нас есть тот же KEY и JavaScript, который был напечатан, может закончиться этим очень странным образом. работает только в нескольких случаях, когда у нас есть полный контроль над данными, которые очень немногие, как это приложение.
Ответ 3
Я начал писать пакет, чтобы сделать его более простым и универсальным. Также для повышения производительности. Он все еще находится на ранних стадиях (38% охвата). Вот небольшой фрагмент (если вы можете использовать новые функции ES6), но есть и альтернативы.
import { create_store } from 'redux';
import { create_reducer, redup } from 'redux-decorator';
class State {
@redup("Todos", "AddTodo", [])
addTodo(state, action) {
return [...state, { id: 2 }];
}
@redup("Todos", "RemoveTodo", [])
removeTodo(state, action) {
console.log("running remove todo");
const copy = [...state];
copy.splice(action.index, 1);
return copy;
}
}
const store = createStore(create_reducer(new State()));
Вы также можете даже вложить свое состояние:
class Note{
@redup("Notes","AddNote",[])
addNote(state,action){
//Code to add a note
}
}
class State{
aConstant = 1
@redup("Todos","AddTodo",[])
addTodo(state,action){
//Code to add a todo
}
note = new Note();
}
// create store...
//Adds a note
store.dispatch({
type:'AddNote'
})
//Log notes
console.log(store.getState().note.Notes)
Доступно много документации по НПМ. Как всегда, не стесняйтесь вносить свой вклад!
Ответ 4
Производительность не очень много. Но с точки зрения дизайна немало. Имея несколько редукторов, вы можете иметь разделение проблем - каждый модуль касается только самих себя. Имея создателей действий, вы добавляете слой косвенности -allowing, чтобы сделать изменения более легко. В конце концов это все еще зависит, если вам не нужны эти функции, общее решение помогает сократить код.
Ответ 5
Прежде всего, некоторая терминология:
- action: сообщение, которое мы хотим отправить всем редукторам. Это может быть что угодно. Обычно это простой объект Javascript, такой как
const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
- Тип действия: константа, используемая создателями действия для создания действия, и редукторами, чтобы понять, какое действие они только что получили. Вы используете их, чтобы избежать ввода
'SOME_ACTION'
как в создателях действий, так и в редукторах. Вы определяете тип действия, такой как const SOME_ACTION = 'SOME_ACTION'
чтобы его можно было import
в создателях действия и в редукторах. - action creator: функция, которая создает действие и отправляет его редукторам.
- редуктор: функция, которая получает все действия, отправленные в хранилище, и отвечает за обновление состояния для этого хранилища redux (у вас может быть несколько магазинов, если ваше приложение является сложным).
Теперь на вопрос.
Я думаю, что универсальный создатель действия не является отличной идеей.
В вашем приложении может потребоваться использование следующих создателей действий:
fetchData()
fetchUser(id)
fetchCity(lat, lon)
Реализация логики работы с другим числом аргументов в одном создателе действия для меня не подходит.
Я думаю, что гораздо лучше иметь много мелких функций, потому что они имеют разные обязанности. Например, fetchUser
не должен иметь ничего общего с fetchCity
.
Я начинаю с создания модуля для всех моих типов действий и создателей действий. Если мое приложение растет, я могу разделить создателей действий на разные модули (например, actions/user.js
, actions/cities.js
), но я думаю, что наличие отдельных модулей /s для типов действий немного перебор.
Что касается редукторов, я считаю, что один редуктор является жизнеспособным вариантом, если вам не нужно заниматься слишком большим количеством действий.
Редуктор получает все действия, отправленные создателями действия. Затем, посмотрев на action.type
, он создает новое состояние магазина. Так как в любом случае вам приходится иметь дело со всеми входящими действиями, мне приятно иметь всю логику в одном месте. Это, конечно, становится сложным, если ваше приложение растет (например, switch/case
для обработки 20 различных действий не очень поддерживается).
Вы можете начать с одного редуктора, перейти к нескольким редукторам и объединить их в корневой редуктор с функцией combineReducer
.
Ответ 6
Хорошо, я просто напишу свой собственный ответ:
-
при использовании redux задайте себе следующие два вопроса:
- Нужен ли мне доступ к данным через несколько компонентов?
-
Являются ли эти компоненты на другом дереве узлов? Я имею в виду, что это не дочерний компонент.
Если ваш ответ "да", то используйте эти сокращения для этих данных, так как вы можете легко передать эти данные своим компонентам через API-интерфейс connect()
который в терминах делает их containers
.
-
Иногда, если вы обнаружите необходимость передачи данных в родительский компонент, вам необходимо пересмотреть, где живет ваше государство. Есть вещь под названием " Поднятие состояния вверх".
-
Если ваши данные имеют значение только для вашего компонента, вам следует использовать setState
чтобы ваша область была ограничена. Пример:
class MyComponent extends Component {
constructor() {
super()
this.state={ name: 'anonymous' }
}
render() {
const { name } = this.state
return (<div>
My name is { name }.
<button onClick={()=>this.setState({ name: 'John Doe' })}>show name</button>
</div>)
}
}
- Также не забывайте поддерживать однонаправленный поток данных. Не просто подключайте компонент к хранилищу redux, если вначале данные уже доступны его родительскому компоненту следующим образом:
<ChildComponent yourdata={yourdata} />
- Если вам нужно изменить родительское состояние от дочернего, просто передайте контекст функции логике вашего дочернего компонента. Пример:
В родительском компоненте
updateName(name) {
this.setState({ name })
}
render() {
return(<div><ChildComponent onChange={::this.updateName} /></div>)
}
В дочернем компоненте
<button onClick={()=>this.props.onChange('John Doe')}
Вот хорошая статья об этом.
Ответ 7
Ключевое решение, которое необходимо предпринять при разработке программ React/Redux, - это то, куда поместить бизнес-логику (она должна куда-то идти!).
Это может быть в компонентах React, в создателях действия, в редукторах или в сочетании с ними. Является ли общая комбинация действий/редукторов разумной, зависит от того, куда идет бизнес-логика.
Если компоненты React выполняют большую часть бизнес-логики, то создатели действий и редукторы могут быть очень легкими и могут быть помещены в один файл, как вы предлагаете, без каких-либо проблем, за исключением того, что компоненты React более сложны.
Причина, по которой большинство проектов React/Redux, похоже, содержит множество файлов для создателей действий и редукторов, потому что некоторые бизнес-логики вставляются туда, и это приведет к очень раздутому файлу, если использовался общий метод.
Лично я предпочитаю иметь очень простые редукторы и простые компоненты и иметь большое количество действий, чтобы абстрагировать сложность, например, запрашивать данные из веб-службы в создателях действия, но "правильный" способ зависит от проекта.
Быстрое примечание. Как упоминалось в fooobar.com/info/15758468/..., объект должен быть возвращен из setState
. Это связано с тем, что некоторая асинхронная обработка может потребоваться до store.dispatch
.
Ниже приведен пример сокращения шаблона. Здесь используется общий редуктор, который уменьшает необходимый код, но возможно только, что логика обрабатывается в другом месте, чтобы действия делались как можно проще.
import ActionType from "../actionsEnum.jsx";
const reducer = (state = {
// Initial state ...
}, action) => {
var actionsAllowed = Object.keys(ActionType).map(key => {
return ActionType[key];
});
if (actionsAllowed.includes(action.type) && action.type !== ActionType.NOP) {
return makeNewState(state, action.state);
} else {
return state;
}
}
const makeNewState = (oldState, partialState) => {
var newState = Object.assign({}, oldState);
const values = Object.values(partialState);
Object.keys(partialState).forEach((key, ind) => {
newState[key] = values[ind];
});
return newState;
};
export default reducer;
TL;DR Это дизайнерское решение, которое должно быть принято на ранней стадии разработки, поскольку оно влияет на то, как структурирована большая часть программы.