React/Redux—组件的每个实例的单独状态
React/Redux—Individual state for each Instance of a Component
如果有一个用户列表并且每个条目都有一个按钮 »EDIT«。如果用户点击它,会发生以下情况:
- 请求表单的服务器
- 将组件
<UserEditForm />
添加到条目中,什么扩展了条目
除一件事外,这工作正常:如果单击更多按钮,则每个表单实例都会收到请求的最后一个用户表单的数据。那是因为我在州里只有一个userform
属性
所以为了解决这个问题,我想将 userform
交换为 userforms
,其中 should/could 是一个像这样的对象:
userforms: {
<id of first instance>: { … }, //formdata
<id of second instance>: { … },
…
}
但由于我是 React/Redux 的新手,所以我真的不知道该怎么做,或者真正做到这一点的 »正确« 方法或最佳实践是什么。
我的想法是像这样创建一个更高阶的组件:
import React from 'react';
import {connect} from 'react-redux';
import {uuid} from '../../helpers/uuid';
export const formdatainstance = (FormInstance) => {
let id = null;
class FormDataMapper extends React.Component {
constructor (props) {
super(props);
id = uuid();
}
render () {
//extract the formdata from the state
//using the id
return <FormInstance { ...this.props } />
}
}
const mapStateToProps = (state) => {
console.log(id); //is null for one run
return {
userforms: state.userforms
};
};
return connect(mapStateToProps)(FormDataMapper);
}
所以在列表组件中我可以:
import UserEditForm from './UserEditForm';
import {formdatainstance} from './formdatainstance';
const MappedUserEditForm = formdatainstance(UserEditForm);
class List extends React.Component {
render(){
return (
{users.map(user => {
//more stuff
<MappedUserEditForm />
//more stuff
})}
);
}
}
所以我的问题是:这是个好主意吗?如果是的话,进行清理的正确方法是什么,那么我应该在组件的生命周期中什么时候从状态中删除数据?有没有其他方法可以做到这一点,哪个更容易?
感谢您的帮助!
您可以执行以下操作...
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm } from 'redux-form';
class UserEditForm extends Component {
...
render() {
return <form onSubmit={this.props.handleSubmit(this.props.onSubmit)}>
...form fields
</form>
}
}
const mapStateToProps = (state, ownProps) => {
return {
form: ownProps.formId
}
}
export default compose(
connect(mapStateToProps),
reduxForm({
//...other redux-form options
})
)(UserEditForm);
您的 ListComponent
render() {
return <ul>
{this.props.users.map(user =>
<li key={user.id}>
...
<UserEditForm formId={'form-' + user.id} onSubmit={...} />
</li>
)}
</ul>
}
这样您就可以拥有一个动态的表单名称。
即使@jpdelatorre 的答案对我来说似乎是最好的,因为它还包括 link 到 redux-forms,这可能对我有很大帮助,我想 post 我的工作解决方案在这里,以防万一有人发现它有用。一夜之间就被它击中了,所以需要测试我的想法是否正确,我最终可以证明什么。
我无法用一个单一的 HOC 完成整个映射,我也需要 add/modify 减速器。基本上它是这样工作的:
数据映射是通过ID来完成的,
包装原始动作创建者,以便将使用的 id 附加到对象
reducer 被包装成两个并由 »datamapped« reducer 调用
原来reducer和action creator的代码不用改,wrapping类的好用。我首先想使用动态创建的 uuid
,但我放弃了它,以便可以保存和恢复整个应用程序状态。
所以 HOC 代码是:
import React from 'react';
import {connect} from 'react-redux';
// The Component to wrap,
// all of its actions
// its default state
export const formdatainstance = (FormInstance, Actions, defaultState = {}) => {
const mapStateToProps = (state) => {
return {
mappedData: state.mappedData
};
};
class FormDataMapper extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired
};
static contextTypes = {
store: React.PropTypes.object
};
//most of mapping happens here
render () {
//wrap the action creators
const actions = Object.keys(Actions).reduce((list, key) =>{
list[key] = (...args) => {
const action = Actions[key](...args);
//handle asyn operations as well
if('then' in action && typeof action['then'] == 'function') {
action.then(data => {
//attaching the id
this.props.dispatch({...data, id: this.props.id});
});
} else {
//attach the id
this.context.store.dispatch({...action, id: this.props.id });
}
};
return list;
}, {}),
//there wont be any data at first, so the default state is handed
//over
mappedProps = this.props.mappedData.hasOwnProperty(this.props.id) ?
this.props.mappedData[this.props.id] : defaultState;
//merge the hotchpotch
let props = Object.assign({}, mappedProps, this.props, actions);
//clean up
delete props.id;
delete props.mappedData;
return <FormInstance { ...props } />
}
}
return connect(mapStateToProps)(FormDataMapper);
};
reducer代码:
//hlper method
export const createTypesToReducerMap = (types, reducer) => {
return Object.keys(types).reduce((map, key) => {
map[types[key]] = reducer;
return map;
}, {});
}
export const createMappedReducer = (reducerMap, defaultState = {}) => {
const HANDLERS = reducerMap.reduce((handlers, typeMap) => {
return { ...handlers, ...typeMap };
},{});
return (state, action) => {
if (!action.hasOwnProperty('id')) {
if (state === undefined) return defaultState;
return state;
}
const reducer = HANDLERS.hasOwnProperty(action.type) ?
HANDLERS[action.type] : null;
let a = {...action};
delete a.id;
return reducer !== null ?
Object.assign({}, state, { [action.id]: reducer(state[action.id], a)}) :
state;
}
}
最后是商店:
const userEditTypeReducerMap = createTypesToReducerMap(userEditTypes, userFormReducer);
const reducer = combineReducers({
…
mappedData: createMappedReducer(
[userEditTypeReducerMap], {})
…
});
export default compose(
applyMiddleware(
thunk
)
)(createStore)(reducer, {});
如果有一个用户列表并且每个条目都有一个按钮 »EDIT«。如果用户点击它,会发生以下情况:
- 请求表单的服务器
- 将组件
<UserEditForm />
添加到条目中,什么扩展了条目
除一件事外,这工作正常:如果单击更多按钮,则每个表单实例都会收到请求的最后一个用户表单的数据。那是因为我在州里只有一个userform
属性
所以为了解决这个问题,我想将 userform
交换为 userforms
,其中 should/could 是一个像这样的对象:
userforms: {
<id of first instance>: { … }, //formdata
<id of second instance>: { … },
…
}
但由于我是 React/Redux 的新手,所以我真的不知道该怎么做,或者真正做到这一点的 »正确« 方法或最佳实践是什么。
我的想法是像这样创建一个更高阶的组件:
import React from 'react';
import {connect} from 'react-redux';
import {uuid} from '../../helpers/uuid';
export const formdatainstance = (FormInstance) => {
let id = null;
class FormDataMapper extends React.Component {
constructor (props) {
super(props);
id = uuid();
}
render () {
//extract the formdata from the state
//using the id
return <FormInstance { ...this.props } />
}
}
const mapStateToProps = (state) => {
console.log(id); //is null for one run
return {
userforms: state.userforms
};
};
return connect(mapStateToProps)(FormDataMapper);
}
所以在列表组件中我可以:
import UserEditForm from './UserEditForm';
import {formdatainstance} from './formdatainstance';
const MappedUserEditForm = formdatainstance(UserEditForm);
class List extends React.Component {
render(){
return (
{users.map(user => {
//more stuff
<MappedUserEditForm />
//more stuff
})}
);
}
}
所以我的问题是:这是个好主意吗?如果是的话,进行清理的正确方法是什么,那么我应该在组件的生命周期中什么时候从状态中删除数据?有没有其他方法可以做到这一点,哪个更容易?
感谢您的帮助!
您可以执行以下操作...
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm } from 'redux-form';
class UserEditForm extends Component {
...
render() {
return <form onSubmit={this.props.handleSubmit(this.props.onSubmit)}>
...form fields
</form>
}
}
const mapStateToProps = (state, ownProps) => {
return {
form: ownProps.formId
}
}
export default compose(
connect(mapStateToProps),
reduxForm({
//...other redux-form options
})
)(UserEditForm);
您的 ListComponent
render() {
return <ul>
{this.props.users.map(user =>
<li key={user.id}>
...
<UserEditForm formId={'form-' + user.id} onSubmit={...} />
</li>
)}
</ul>
}
这样您就可以拥有一个动态的表单名称。
即使@jpdelatorre 的答案对我来说似乎是最好的,因为它还包括 link 到 redux-forms,这可能对我有很大帮助,我想 post 我的工作解决方案在这里,以防万一有人发现它有用。一夜之间就被它击中了,所以需要测试我的想法是否正确,我最终可以证明什么。
我无法用一个单一的 HOC 完成整个映射,我也需要 add/modify 减速器。基本上它是这样工作的:
数据映射是通过ID来完成的,
包装原始动作创建者,以便将使用的 id 附加到对象
reducer 被包装成两个并由 »datamapped« reducer 调用
原来reducer和action creator的代码不用改,wrapping类的好用。我首先想使用动态创建的 uuid
,但我放弃了它,以便可以保存和恢复整个应用程序状态。
所以 HOC 代码是:
import React from 'react';
import {connect} from 'react-redux';
// The Component to wrap,
// all of its actions
// its default state
export const formdatainstance = (FormInstance, Actions, defaultState = {}) => {
const mapStateToProps = (state) => {
return {
mappedData: state.mappedData
};
};
class FormDataMapper extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired
};
static contextTypes = {
store: React.PropTypes.object
};
//most of mapping happens here
render () {
//wrap the action creators
const actions = Object.keys(Actions).reduce((list, key) =>{
list[key] = (...args) => {
const action = Actions[key](...args);
//handle asyn operations as well
if('then' in action && typeof action['then'] == 'function') {
action.then(data => {
//attaching the id
this.props.dispatch({...data, id: this.props.id});
});
} else {
//attach the id
this.context.store.dispatch({...action, id: this.props.id });
}
};
return list;
}, {}),
//there wont be any data at first, so the default state is handed
//over
mappedProps = this.props.mappedData.hasOwnProperty(this.props.id) ?
this.props.mappedData[this.props.id] : defaultState;
//merge the hotchpotch
let props = Object.assign({}, mappedProps, this.props, actions);
//clean up
delete props.id;
delete props.mappedData;
return <FormInstance { ...props } />
}
}
return connect(mapStateToProps)(FormDataMapper);
};
reducer代码:
//hlper method
export const createTypesToReducerMap = (types, reducer) => {
return Object.keys(types).reduce((map, key) => {
map[types[key]] = reducer;
return map;
}, {});
}
export const createMappedReducer = (reducerMap, defaultState = {}) => {
const HANDLERS = reducerMap.reduce((handlers, typeMap) => {
return { ...handlers, ...typeMap };
},{});
return (state, action) => {
if (!action.hasOwnProperty('id')) {
if (state === undefined) return defaultState;
return state;
}
const reducer = HANDLERS.hasOwnProperty(action.type) ?
HANDLERS[action.type] : null;
let a = {...action};
delete a.id;
return reducer !== null ?
Object.assign({}, state, { [action.id]: reducer(state[action.id], a)}) :
state;
}
}
最后是商店:
const userEditTypeReducerMap = createTypesToReducerMap(userEditTypes, userFormReducer);
const reducer = combineReducers({
…
mappedData: createMappedReducer(
[userEditTypeReducerMap], {})
…
});
export default compose(
applyMiddleware(
thunk
)
)(createStore)(reducer, {});