If have a list of users and each Entry has a button »EDIT«. If the user clicks on it the following happens:
- request the server for the form
- Add the component
<UserEditForm />
to the entry, what expands the entry
This works fine except one thing: If one clicks further buttons each Instance of the form receives the data of the last user form requested. That is because I have only only one userform
property in the state.
So to solve this I want to exchange userform
to userforms
which should/could be an Object like that:
userforms: {
<id of first instance>: { … }, //formdata
<id of second instance>: { … },
…
}
But since I am new to React/Redux I do not really know how to do that, or what the »right« approach, or best practice is, to actually do it.
My Idea is to create a higher Order Component like so:
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);
}
So in the List component I can:
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
})}
);
}
}
So my Question: Is this a good Idea? If yes what would be the proper way to do the cleanup, so when in the life cycle of the component should I delete the data from the state? Is there another way to do that, which is easier?
Thanks for Help!
Here's what you can do...
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);
Your ListComponent
render() {
return <ul>
{this.props.users.map(user =>
<li key={user.id}>
...
<UserEditForm formId={'form-' + user.id} onSubmit={...} />
</li>
)}
</ul>
}
This allows you to have a dynamic form name.
Even if the answer of @jpdelatorre seems to be the best hit for me, since it also includes the link to redux-forms, what will probably help me a lot, I would like to post my working solution here, just in case somebody might find it useful. It just hit me over night, so needed to test if my thought were right, what I could finally proof.
I was not able to do the whole Mapping with a sole HOC and I needed to add/modify reducers too. Basically it works that way:
Data Mapping is done by ID,
the original action creators are wrapped, such that the used id is attached to the Object
the reducers are wrapped two and called by the »datamapped« reducer
So the code of the original reducers and action creators does not need to be changed, what makes the wrapping kind of easy to use. I first wanted to use uuid
's which are created on the fly, but I discarded that, to make possible to save and restore the whole application state.
so the HOC code is that:
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);
};
the reducer code:
//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;
}
}
and finally the store:
const userEditTypeReducerMap = createTypesToReducerMap(userEditTypes, userFormReducer);
const reducer = combineReducers({
…
mappedData: createMappedReducer(
[userEditTypeReducerMap], {})
…
});
export default compose(
applyMiddleware(
thunk
)
)(createStore)(reducer, {});