3
votes

I'm having trouble figuring out how to compose ui widget reducers and react render trees in tandem, in a way that I can later map the resulting redux store for the reducers to props in the render tree using react-redux.

Suppose a setup like this. I'm trying to make a NameWidget that I can use anywhere in any app:

NameView.jsx:
...
render() {
  return <div> Name: {this.props.name} </div>;
}
...

====

NameAction.js:
...
export const CHANGE_NAME = 'CHANGE_NAME';
...
export function changeNameAction(newName) {
  return {
    type: CHANGE_NAME,
    payload: newName,
  };
}

====

NameReducer.js:

import { CHANGE_NAME } from './NameAction';

function ui(state = {name: ''}, action) {
  switch(action.type) {
    case(CHANGE_NAME):
      return Object.assign({}, state, {name: action.payload});
      break;
    default:
      return state;
  }
}

export default ui;

====

NameWidget.js:

import { connect } from 'react-redux';
import { NameView } from './NameView';

const mapStateToProps = (state) => {
  return {
    name: state.name,
  };
};

const NameWidget = connect(
  mapStateToProps
)(NameView);

export default NameWidget;

If I use NameWidget directly I'll have no problem:

NameApp.jsx:

import { createStore } from 'redux';
import app from './NameReducers';
let store = createStore(app);
...
function render() {
  return (
    <Provider store={store}>
      <NameWidget/>
    </Provider>
  );
}

However, I don't see how to make this modular. I can't actually put this Widget into anything else. Suppose I wanted to do this:

EncapsulatingView.jsx:
...
render() {
  return (
    <div>
      <NameWidget/>
      <SomeOtherReduxWidget/>
    </div>
  );
}
...

====

EncapsulatingReducer.js:
import { combineReducers } from 'redux'
import nameWidget from '../name/NameReducer';
import someOtherWidget from '../someOther/SomeOtherReduxWidgetReducer';

export default combineReducers({nameWidget, someOtherWidget});

(I expect that the remaining code including connect() and createStore() for Encapsulating* is obvious.)

This almost works, as long as all the actions across all encapsulated views have unique types. But the problem is NameWidget's mapStateToProps; it needs to change from above to use a new path to its section of the store:

...
const mapStateToProps = (state) => {
  return {
    name: state.nameWidget.name,
  };
};
...

And so on - depending on how ancestors combineReducers() to define their ever-grander super widgets and apps, the descendants somehow have to change how they mapStateToProps() to compensate. This breaks encapsulation and reusability of code, and I don't see how to get around it in react-redux. How can I define widgets by composing combineReducers(), and then map the resulting store to those widgets' props, in a way that's write-once-use-anywhere?

(Apropos of nothing, it seems strange that repeated combineReducers() creates a shape that's bottom up, but that mapStateToProps() seems to assume instead a top-down "absolute path" approach to looking up into that shape.)

2
Do you plan on exporting this code to another project or was your intent on re-using the widget in another setting in the same project?Gennon
Short term is project-specific, but if there's a good pattern/solution hopefully it would support both.jdowdell
Have you had a look redux-form where they makes use of a defined name (form) as their own root-reducer. That way the components will look at state.form for all the values.Gennon

2 Answers

1
votes

Interesting question. I wonder if it would serve your purpose to require a guid be generated by the parent and passed down through props, and then you simply use that as the index of your state object for that instance of your widget. So whenever you are about to create a new instance of this widget you need to create an ID for it in the parent, and then pass that down in the props, then it will be available to the connect method to use in both mapStateToProps and mapDispatchToProps. I suppose in theory you could even create this parent component yourself and it could possibly be transparent when it is being used, and simply use componentDidMount to generate a new guid to store in component state and pass down to the child.

0
votes

I gave John the credit for this question because he essentially provided the right magic for the answer, passing store "routing" logic via mapStateToProps()'s ownProps function arg. But I preferred my own spin on it. Essentially, whenever you combineReducers() in an EncapsulatingReducer.js, you have to remember to pass a uiStorePath prop in EncapsulatingView.js:

New NameWidget.js:

const _ = require('lodash');
import { connect } from 'react-redux';
import { NameView } from './NameView';

const mapStateToProps = (state, props) => {
  const uiStore = (
    (ownProps && _.has(ownProps, 'uiStorePath') && ownProps.uiStorePath &&
        ownProps.uiStorePath.length) ? 
      _.get(state, ownProps.uiStorePath) :  // deep get...old versions of lodash would _.deepGet()
      state
  );

  return {
    name: uiStore.name,
  };
};

const NameWidget = connect(
  mapStateToProps
)(NameView);

export default NameWidget;

====

EncapsulatingView.js:

...
render() {
  const uiStorePathBase = ((this.props.uiStorePath &&         
      this.props.uiStorePath.length) ?
    this.props.uiStorePath + '.' :
    ''
  );
  return (
    <div> 
      <NameWidget uiStorePath={uiStorePathBase+"nameWidget"}/>
      <SomeOtherWidget uiStorePath={uiStorePathBase+"someOtherWidget"/>
    </div>
  );
}
...