упростить сокращение с помощью общего действия и редуктора

В проекте 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. Централизованные файлы со всеми действиями
  2. Централизованные файлы со всеми редукторами

Вопрос в том, почему двухэтапный процесс? Здесь другое архитектурное предложение:

Создайте 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 задайте себе следующие два вопроса:

    1. Нужен ли мне доступ к данным через несколько компонентов?
    2. Являются ли эти компоненты на другом дереве узлов? Я имею в виду, что это не дочерний компонент.

      Если ваш ответ "да", то используйте эти сокращения для этих данных, так как вы можете легко передать эти данные своим компонентам через 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 Это дизайнерское решение, которое должно быть принято на ранней стадии разработки, поскольку оно влияет на то, как структурирована большая часть программы.