React/Redux—Individual state for each Instance of

2019-07-27 07:10发布

问题:

If have a list of users and each Entry has a button »EDIT«. If the user clicks on it the following happens:

  1. request the server for the form
  2. 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!

回答1:

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.



回答2:

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:

  1. Data Mapping is done by ID,

  2. the original action creators are wrapped, such that the used id is attached to the Object

  3. 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, {});