Всем привет! Сегодня рассмотрим решение, довольно-таки популярной проблемы — получение доступа к state из функции mapDispatchToProps() react-redux приложения.


Имеется типовой компонент-контейнер (про идеологию компонентов react-redux можно почитать здесь), который генерирую с помощью функции connect(). Код представлен ниже (публикую кусок кода, относящийся к данной теме):


const mapStateToProps = (state) => {
    return state.play;
};

const mapDispatchToProps = (dispatch) => {
    return {
        togglePlay: () => {
            dispatch(togglePlay());
        }
    }
};

const ButtonPlayComponentContainer = connect(
    mapStateToProps,
    mapDispatchToProps
)(ButtonPlayComponentView);

Тут все просто, определяем функции mapStateToProps() для чтения состояния и mapDispatchToProps() для передачи события. Далее генерируем компонент путем передачи созданных функций в connect().


В добавок публикую код метода render() компонента-представления, для более ясной картины:


render() {
    return(
        <div className="button-play" onClick={this.props.togglePlay}>
            <i className={ this.props.play == false ? "fa fa-play" : "fa fa-pause" }></i>
        </div>
    );
};

Обычная ситуация, при клике на кнопку, меняется state и в зависимости от состояния, меняется класс у элемента.


Но теперь появляется задача, при изменении состояния, возвращать ту или иную функцию. Вроде бы не сложно, проблема решается одним if, но есть одно но. У нас нет доступа к state в методе mapDispatchToProps(). С лету, на ум приходит сразу один вариант — сделать запрос к хранилищу с помощью метода getState() и получить текущее состояние. Но такой вариант смутил меня своей бестолковостью. Ибо пропадает весь смысл в функции mapStateToProps, которая и так отвечает за состояние.


Просмотрев документацию по методу connect() (на этот раз внимательно), обнаружил параметр mergeProps:


connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]);

Выдержка из документации по данному параметру:


You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props.

Если дословно переводить, но получается, что данная функция дает возможность получить текущее состояние, либо передавать события путем привязки нашего экшена к переменной из props (то что делает mapDispatchToProps()). Отлично, убиваем двух зайцев одним выстрелом.


Немного погуглив, по теме реализации метода mergeProps, наткнулся на вопрос на github.


В итоге, получаем:


const mapStateToProps = (state) => {
    return state.play;
};

const mergeProps = (stateProps, dispatchProps) => {
    const { play } = stateProps;
    const { dispatch } = dispatchProps;

    const toggle = () => {
        dispatch(togglePlay());
        if (play != true) {
            this.playAction();
        } else {
            this.stopAction();
        }
    };

    return {
        play: play,
        togglePlay: () => {
            toggle();
        }
    };
};

const ButtonPlayComponentContainer = connect(
    mapStateToProps,
    null,
    mergeProps
)(ButtonPlayComponentView);

Тут тоже все просто, в mergeProps прилетают stateProps, который содержит текущее состояние и dispatchProps, который дает возможность отправить событие. Далее по коду делаем проверку на состояние, результатом которой будет нужная функция и возвращаем объект с текущим state и событием, который благополучно попадет в props нашего компонента-представления.


Если обнаружили какие-нибудь недочеты, пишите, поправлю. Спасибо за внимание.

Поделиться с друзьями
-->

Комментарии (10)


  1. MrCheater
    07.11.2016 14:33
    +3

    В новой версии react-redux эта проблема и другие похожие решается более элегантно через


    connectAdvanced(selectorFactory, [connectOptions])

    https://github.com/reactjs/react-redux/blob/next/docs/api.md#connectadvancedselectorfactory-connectoptions
    Там можно более гибко конфигурировать props для вложенного компонента.


    Пример кода:


    import * as actionCreators from './actionCreators'
    import { bindActionCreators } from 'redux'
    
    function selectorFactory(dispatch) {
      let state = {}
      let ownProps = {}
      let result = {}
      const actions = bindActionCreators(actionCreators, dispatch)
      const addTodo = (text) => actions.addTodo(ownProps.userId, text)
      return (nextState, nextOwnProps) => {
        const todos = nextState.todos[nextProps.userId]
        const nextResult = { ...nextOwnProps, todos, addTodo }
        state = nextState
        ownProps = nextOwnProps
        if (!shallowEqual(result, nextResult)) result = nextResult
        return result
      }
    }
    export default connectAdvanced(selectorFactory)(TodoApp)


  1. ookami_kb
    07.11.2016 17:29

    А я в таких случаях обычно передаю срез нужного мне состояния из компонента, что-то типа onClick={(myState) => this.props.togglePlay(myState)}. Или так делать – bad practice?


    1. VasilioRuzanni
      07.11.2016 17:41

      Bad practice, потому что тогда компонент должен знать о структуре стейта слишком много — больше, чем ему полагается для своей функциональности. То же касается и селекторов. В простых приложениях это может работать, но с ростом количества фич это становится все сложнее поддерживать (при изменении структуры state'а приходится вносить изменения в компоненты, селекторы и так далее).


      1. ookami_kb
        07.11.2016 17:51
        +1

        Ну можно же передавать туда и просто свои props – тогда это ничем не будет отличаться от способа с mergeProps.


        1. VasilioRuzanni
          08.11.2016 11:12

          Я думал, что речь о том, что стейт разбирается внутри, скажем, togglePlay(). Если myState — это уже нужный компоненту кусок стейта — это неплохо. Однако bad practice тут в том, что ваш компонент вообще знает, что что-то хранится в каком-то там стейте, вы его передаете туда-cюда. В этом плане еще лучше, если компонент вообще не будет знать, откуда берутся props (из стейта, переданы вручную, и так далее), это позволяет его легко переиспользовать. А ко всему прочему это проще тестировать.

          Ну можно же передавать туда и просто свои props
          Куда «туда»? :)

          Кстати, как у вас в примере 'myState' оказывается аргументом обработчика onClick? Там же SyntheticEvent.


          1. ookami_kb
            08.11.2016 11:50

            В этом плане еще лучше, если компонент вообще не будет знать, откуда берутся props (из стейта, переданы вручную, и так далее)

            Ну да, компонент знает только то, что мы ему передали в props. Но, соответственно, он может и передать в функцию-обработчик какую-то часть этих props.


            Кстати, как у вас в примере 'myState' оказывается аргументом обработчика onClick? Там же SyntheticEvent.

            Да, затупил, конечно же. Должно быть что-то такое:


            onClick={(e) => this.props.togglePlay(this.props.play)}


            1. VasilioRuzanni
              08.11.2016 13:23
              +1

              А, в таком случае — вообще никаких нареканий, так можно и это, в общем-то, не bad practice. Тут скорее наоборот — иметь в props экшены с уже «прошитыми» props бывает просто удобно, чтобы каждый раз не прокидывать их внутри компонента. Я сам тоже предпочитаю эту логику убирать за его пределы, оставляя презентационному (presentational/dumb) компоненту минимум простора для принятия подобных решений.


  1. n0ne
    07.11.2016 18:06
    -3

    По-моему, redux-thunk как-то всё-таки понятнее…


  1. NeXTs_od
    07.11.2016 18:28
    +1

    const toggle = () => {
        dispatch(togglePlay());
        ...
    


    где объявлен togglePlay?


    1. Venom4eg
      16.11.2016 00:48

      В actions, видимо.