如何将redux存储区中的封装ui状态映射到react-redux的道具?

问题描述:

我无法弄清楚如何编写ui窗口小部件缩减器并且同时对渲染树进行反应,这样我就可以稍后使用react-redux将缩减器的结果redux存储库映射到渲染树中的道具。如何将redux存储区中的封装ui状态映射到react-redux的道具?

假设像这样的设置。我试图做一个NameWidget,我可以在任何应用程序在任何地方使用:

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; 

如果我直接使用NameWidget,我将没有问题em:

NameApp.jsx: 

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

但是,我没有看到如何使这个模块化。我实际上无法将这个Widget放入其他任何东西中。假设我想这样做:

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}); 

(我预计剩余的代码,包括connect()和createStore()用于封装*是显而易见的。)

几乎工作,只要所有封装的视图中的所有操作具有唯一类型。但问题是NameWidget的mapStateToProps;它需要从上面改为使用新的路径,以它的商店的部分:

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

等等 - 在某种程度上取决于祖先combineReducers如何()来定义其不断宏伟的超级小工具和应用程序,后裔必须改变mapStateToProps()的补偿方式。这打破了代码的封装和可重用性,我看不到如何在react-redux中解决它。我怎样才能通过组合combineReducers()来定义小部件,然后将结果存储映射到这些小部件的道具上,这种方式在任何地方都是一次写入使用的?

(没有任何答案,似乎很奇怪重复的combineReducers()会创建一个自下而上的形状,但是mapStateToProps()似乎会采用自顶向下的“绝对路径”方法来查找该形状。

+0

你打算在这个出口代码到另一个项目,或者您是否打算在同一个项目的另一个设置中重新使用该小部件? – Gennon

+0

短期是项目特定的,但如果有一个好的模式/解决方案,希望它会支持这两种。 – jdowdell

+0

你有没有看过[redux-form](http://redux-form.com/5.3.1/#/getting-started),他们使用一个定义的名称(表单)作为他们自己的root-reducer。这样,组件将查看所有值的state.form。 – Gennon

有趣的问题。我想知道它是否会满足您的目的,要求由父级生成GUID并通过道具传递,然后您只需将其用作您的窗口小部件实例的状态对象的索引。因此,无论何时您要创建此小部件的新实例,您都需要在父项中为其创建一个ID,然后在道具中传递该ID,然后将其用于connect方法以用于mapStateToProps和mapDispatchToProps 。我想在理论上你甚至可以自己创建这个父组件,它在使用时可能是透明的,并且只需使用componentDidMount来生成一个新的GUID以存储在组件状态并传递给子组件。

+0

谢谢约翰!我曾想过类似的东西,但我不知道如何“在道具中传递这些东西” - 你能详细说明一下吗?在我看来,connect()基本上是在我导入它返回的类型时调用的,而不是在我使用该类型时的render()中。我想我可以将connect()包装在函数中,然后在render()过程中动态调用它,并将该函数传递给您提到的ID;但我不确定每个调用渲染动态生成的类型的影响...... – jdowdell

+0

所以我说你会有一个父组件,并在componentDidMount(实际上componentWillMount可能是更好的地方),你本质上调用'this.setState({childId:newGuid()})',然后在渲染方法中使用''来呈现子容器组件。通过容器组件,我只是指一个'redux only'组件,它是调用'connect'与你的实际子组件的结果。 – John

+0

然后在传递函数mapStateToProps和mapDispatchToProps的参数时,在你的连接函数中,在_those_函数中使用它们的第二个参数(它是组件的道具)来使用id。例如,您的连接功能可能如下所示:https://jsbin.com/joyodaxazi/1/edit?js – John

我给了约翰这个问题的功劳,因为他基本上为答案提供了正确的魔法,通过mapStateToProps()的ownProps函数arg传递商店“路由”逻辑。但我更喜欢自己的旋律。从本质上讲,当你在EncapsulatingReducer.js combineReducers(),你要记住传递一个uiStorePath道具在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> 
); 
} 
...