jdowdell jdowdell - 4 months ago 36
React JSX Question

How to map encapsulated ui state in a redux store to props with react-redux?

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.)

Answer

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.