it-roy-ru.com

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

В проекте 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's 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 не нужен! И вы даже можете проверить функции по одной. Так что тестируемость не потеряно.

11
Maria

Во-первых, вместо вызова 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), чем setState({ someField: someValue })}.

2. Более высокая тестируемость

Вы можете легко проверить setSomeField, чтобы убедиться, что он корректно изменяет только связанное состояние. 

С универсальным setState вы также можете проверить setState({ someField: someValue })}, но нет прямой гарантии, что весь ваш код будет вызывать его правильно.
Например. кто-то из вашей команды может сделать опечатку и вместо этого вызвать setState({ someFeild: someValue })}.

Заключение

Недостатки не очень существенны, поэтому вполне можно использовать универсального создателя действий, чтобы уменьшить количество создателей специализированных действий, если вы считаете, что это стоит компромисса для вашего проекта.

Правка

Что касается вашего предложения поместить редукторы и действия в один файл: обычно предпочтительнее хранить их в отдельных файлах для модульности ; это общий принцип, который не является уникальным для React. 

Однако вы можете поместить связанные файлы редуктора и действия в одну и ту же папку, что может быть лучше/хуже в зависимости от требований вашего проекта. Смотрите это и это для некоторого фона. 

Вам также нужно будет экспортировать userReducer для вашего корневого редуктора, если вы не используете несколько хранилищ, что как правило, не рекомендуется

5
riwu

Я начал писать пакет , чтобы сделать его более простым и общим. Также для улучшения производительности. Он все еще находится на ранних стадиях (охват 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)

Много документации доступно на NPM. Как всегда, не стесняйтесь вносить свой вклад!

1
Eladian

В основном я использую избыточность для кеширования ответов API, в некоторых случаях я думал, что это ограничено.

1) Что если я вызываю другой API, который имеет тот же ключ , но идет к другому объекту?

2) Как я могу позаботиться о том, чтобы данные были потоком из сокета? Нужно ли повторять объект, чтобы получить тип (так как тип будет в заголовке и ответ в полезной нагрузке) или попросить мой внутренний ресурс отправить его с определенной схемой.

3) Это также не работает для API, если мы используем какого-либо стороннего поставщика, у которого нет контроля над выходом, который мы получаем.

Всегда хорошо иметь контроль над тем, куда и куда направляются данные . В приложениях, которые очень велики, например, в приложениях для мониторинга сети мы можем закончить перезаписью данных, если у нас будет тот же самый ключ KEY и потерянный JavaScript, это может закончиться очень странным образом это работает только в тех немногих случаях, когда мы имеем полный контроль над данными, которые очень мало похожи на это приложение. 

1
karthik

Хорошо, я просто напишу свой собственный ответ:

  • при использовании приставки задайте себе эти два вопроса:

    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>)
   }
}
  • Также не забывайте поддерживать однонаправленный поток данных. Не просто подключите компонент к избыточному хранилищу, если в первую очередь данные уже доступны его родительскому компоненту, например так:
<ChildComponent yourdata={yourdata} />
  • Если вам нужно изменить родительское состояние дочернего, просто передайте контекст функции в логику вашего дочернего компонента. Пример:

В родительском компоненте

updateName(name) {
    this.setState({ name })
}

render() {
    return(<div><ChildComponent onChange={::this.updateName} /></div>)
}

В дочернем компоненте

<button onClick={()=>this.props.onChange('John Doe')}

Вот хорошая статья об этом.

0
Lelouch

Ключевое решение, которое нужно принять при разработке программ React/Redux, - это где разместить бизнес-логику (она должна куда-то идти!).

Это может быть в компонентах React, в создателях действий, в редукторах или в их комбинации. Является ли общая комбинация действие/редуктор разумной, зависит от того, куда идет бизнес-логика.

Если компоненты React выполняют большую часть бизнес-логики, то создатели действий и редукторы могут быть очень легкими и могут быть помещены в один файл, как вы предлагаете, без каких-либо проблем, кроме как сделать компоненты React более сложными.

Причина в том, что большинство проектов React/Redux, кажется, имеют много файлов для создателей действий и редукторов, потому что там содержится некоторая часть бизнес-логики, и это привело бы к очень раздутому файлу, если бы использовался универсальный метод.

Лично я предпочитаю иметь очень простые редукторы и простые компоненты и иметь большое количество действий для отвлечения сложности, таких как запрос данных из веб-службы в создателей действий, но «правильный» путь зависит от проекта под рукой.

Краткое примечание. Как упоминалось в https://stackoverflow.com/a/50646935 , объект должен быть возвращен из 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;

tldr Это проектное решение, которое должно быть принято на ранней стадии разработки, поскольку оно влияет на структуру большой части программы.

0
A Jar of Clay

Прежде всего, некоторая терминология :

  • действие: сообщение, которое мы хотим отправить в все редукторы. Это может быть что угодно. Обычно это простой объект Javascript, такой как const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
  • тип действия: константа, используемая создателями действия для создания действия и редукторами, чтобы понять, какое действие они только что получили. Вы используете их, чтобы избежать ввода 'SOME_ACTION' как в создателях действий, так и в редукторах. Вы определяете тип действия, такой как const SOME_ACTION = 'SOME_ACTION', так что вы можете import его в создателях действий и в редукторах.
  • action creator: функция, которая создает действие и отправляет его редукторам.
  • reducer: функция, которая получает все действия, отправленные в хранилище, и она отвечает за обновление состояния для этого избыточного store (у вас может быть несколько хранилищ, если ваше приложение сложный).

Теперь к вопросу.

Я думаю, что создатель родового действия - не лучшая идея.

Вашему приложению могут понадобиться следующие создатели действий:

fetchData()
fetchUser(id)
fetchCity(lat, lon)

Реализация логики работы с разным количеством аргументов в одном создателе действий не подходит мне.

Я думаю, что гораздо лучше иметь много маленьких функций, потому что у них разные обязанности. Например, fetchUser не должен иметь ничего общего с fetchCity.

Я начинаю с создания модуля для всех моих типов действий и создателей действий. Если мое приложение растет, я могу разделить создателей действий на разные модули (например, actions/user.js, actions/cities.js), но я думаю, что наличие отдельных модулей для типов действий немного излишне.


Что касается редукторов, я думаю, что один редуктор является жизнеспособным вариантом, если вам не приходится иметь дело с слишком большим количеством действий.

Редуктор получает все действия, отправленные создателями действия. Затем, посмотрев на action.type, он создает новое состояние магазина. Так как вам все равно приходится иметь дело со всеми поступающими действиями, я нахожу приятным иметь всю логику в одном месте. Это, конечно, становится трудным, если ваше приложение расширяется (например, switch/case для обработки 20 различных действий не очень удобен в обслуживании).

Вы можете начать с одного редуктора, перейти к нескольким редукторам и объединить их в корневом редукторе с функцией combineReducer .

0
jackdbd

Производительность мудрая не очень. Но с точки зрения дизайна довольно мало. Имея несколько редукторов, вы можете разделить задачи - каждый модуль касается только себя. Имея создателей действий, вы добавляете слой косвенности, что позволяет вам легче вносить изменения. В конце концов, все еще зависит, если вам не нужны эти функции, универсальное решение помогает сократить объем кода. 

0
miles_christian