React - Multiple components with the same actions

2019-03-02 02:32发布

问题:

I created a component that lets you add/remove additional dropdowns onClick of a button. I use Redux to keep the state of the added fields and value selected.

It works fine but if I add the component twice on the page (using the same actions and reducers), both dropdowns will update at the same time.

How could I make them work independently?

index.jsx

import React from 'react'
import { connect } from 'react-redux'
import DropDownField from './form/drop-down-field'
import uuidV4 from 'uuid-v4'
import { saveSelect, removeSelect, saveSelectValue } from './actions.js'


class Component extends React.Component {
  constructor(props) {
    super(props);
  }

  saveData(e) {
    let data = {}
    data[e.target.name] = e.target.value

    this.context.store.dispatch(
      addData(data)
    )
  }

  addInput = (e) => {
    e.preventDefault()
    this.props.saveSelect({id:uuidV4()})
  }

  removeInput = (index, e) => {
    e.preventDefault()
    this.props.removeSelect(index)
  }

  saveSelectValue = (e, id) => {
    let data = {}
    data.id = id
    data.value = e.target.value

    this.props.saveSelectValue(data)
  }

  renderNationalitiesSelect = (selection, index) => {
    const selectedValue = selection.value || ''
    const id = selection.id

    return(
      <div>
        <DropDownField
          key={id}
          name={'field-'+ id}
          value={selectedValue}
          onChange = {(e) => { this.saveSelectValue(e, id) }}
          required
          options={{
            0: 'Please Select',
            1: 'British',
            2: 'French',
            3: 'American',
            4: 'Australian'
        }}  />

        <a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
      </div>
    )
  }

  renderCountriesSelect = (selection, index) => {
    const selectedValue = selection.value || ''
    const id = selection.id

    return(
      <div>
        <DropDownField
          key={id}
          name={'field-'+ id}
          value={selectedValue}
          onChange = {(e) => { this.saveSelectValue(e, id) }}
          required
          options={{
            0: 'Please Select',
            1: 'United Kingdom',
            2: 'France',
            3: 'United States',
            4: 'Australia'
        }}  />

        <a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
      </div>
    )
  }

  render(){
    const selections = this.props.selections || []

    let {
      Nationality,
      CountryOfResidence
    } = this.props.store

    return (
      <DropDownField name="Nationality" value={Nationality} options={{
        0: 'Please Select', 1: 'British', 2: 'French', 3: 'American', 4: 'Australian'
      }} onChange={this.saveData.bind(this)} />

      <div>
        <div>
          {selections.map(this.renderNationalitiesSelect)}
        </div>

        {this.props.selections.length < 4 &&
          <div>
            <a href="#" onClick={this.addInput}>Add</a>
          </div>
        }
      </div>


      <DropDownField name="CountryOfResidence" value={CountryOfResidence} options={{
        0: 'Please Select', 1: 'United Kingdom', 2: 'France', 3: 'United States', 4: 'Australia'
      }} onChange={this.saveData.bind(this)} />

      <div>
        <div>
          {selections.map(this.renderCountriesSelect)}
        </div>

        {this.props.selections.length < 4 &&
          <div>
            <a href="#" onClick={this.addInput}>Add</a>
          </div>
        }
      </div>

    )
  }
}

const mapStateToProps = (state) => {
  return {
    store: state.AddDropdown,
    selections: state.AddDropdown.selections,
  }
}

const AddDropdown = connect(mapStateToProps, {saveSelect, removeSelect, saveSelectValue})(Component)

export default AddDropdown

action.js

export const ADD_DATA = 'ADD_DATA'
export const ADD_SELECT = 'ADD_SELECT'
export const REMOVE_SELECT = 'REMOVE_SELECT'
export const SAVE_SELECT_OPTION = 'SAVE_SELECT_OPTION'

export function addData(data) {
  return { type: ADD_DATA, data }
}

export function saveSelect(data) {
  return { type: ADD_SELECT, data }
}

export function removeSelect(data) {
  return { type: REMOVE_SELECT, data }
}

export function saveSelectValue(data) {
  return { type: SAVE_SELECT_OPTION, data }
}

reducer.js

import ObjectAssign from 'object.assign'
import { combineReducers } from 'redux'
import { ADD_DATA, ADD_SELECT, REMOVE_SELECT, SAVE_SELECT_OPTION } from './actions'

function AddDropdown(state = { selections: []}, action = {}){
  switch (action.type){
    case ADD_DATA:
      return ObjectAssign({}, state, action.data)
    case ADD_SELECT:
      return {
        ...state,
        selections: [].concat(state.selections, action.data),
      }
    case REMOVE_SELECT:
      return {
        ...state,
        selections: state.selections.filter((selection, index) => (index !== action.data)),
      }
    case SAVE_SELECT_OPTION:
      return {
        ...state,
        selections: state.selections.map((selection) => selection.id === action.data.id ? action.data : selection)
      }
    default:
      return state
  }
}


const FormApp = combineReducers({
  AddDropdown
})

export default FormApp

回答1:

I suggest isolating each set of dropdowns as a seperate component, then working on isolating each one's redux state. My library, redux-subspace was designed for this purpose.

index.jsx

import React from 'react'
import { connect } from 'react-redux'
import { SubspaceProvider } from 'redux-subspace'
import DropDownField from './form/drop-down-field'
import uuidV4 from 'uuid-v4'
import { saveSelect, removeSelect, saveSelectValue } from './actions.js'


class Component extends React.Component {
  constructor(props) {
    super(props);
  }

  saveData(e) {
    let data = {}
    data[e.target.name] = e.target.value

    this.context.store.dispatch(
      addData(data)
    )
  }

  addInput = (e) => {
    e.preventDefault()
    this.props.saveSelect({id:uuidV4()})
  }

  removeInput = (index, e) => {
    e.preventDefault()
    this.props.removeSelect(index)
  }

  saveSelectValue = (e, id) => {
    let data = {}
    data.id = id
    data.value = e.target.value

    this.props.saveSelectValue(data)
  }

  renderSelections = (selection, index) => {
    const selectedValue = selection.value || ''
    const id = selection.id

    return(
      <div>
        <DropDownField
          key={id}
          name={'field-'+ id}
          value={selectedValue}
          onChange = {(e) => { this.saveSelectValue(e, id) }}
          required
          options={this.props.options}  />

        <a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
      </div>
    )
  }

  render(){
    return (
      <div>
        <DropDownField name={this.props.name} value={this.props.store.value} options={this.props.options} onChange={this.saveData.bind(this)} />

        <div>
            {this.props.selections.map(this.renderSelections)}
        </div>

        {this.props.selections.length < 4 &&
            <div>
            <a href="#" onClick={this.addInput}>Add</a>
            </div>
        }
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    store: state,
    selections: state.selections,
  }
}

const SingleAddDropdown = connect(mapStateToProps, {saveSelect, removeSelect, saveSelectValue})(Component)

const AddDropdown = () => {
    return (
        <div>
            <SubspaceProvider mapState={state => state.nationality} namespace="nationalities">
                <SingleAddDropdown name="Nationality" options={{
                    0: 'Please Select',
                    1: 'British',
                    2: 'French',
                    3: 'American',
                    4: 'Australian'
                }}/>
            </SubspaceProvider>
            <SubspaceProvider mapState={state => state.countryOfResidence} namespace="countryOfResidence">
                <SingleAddDropdown name="Country of Residence" options={{ 
                    0: 'Please Select', 
                    1: 'United Kingdom', 
                    2: 'France', 
                    3: 'United States', 
                    4: 'Australia' 
                }}/>
            </SubspaceProvider>
        </div>
    )
}

export default AddDropdown

reducer.js

import ObjectAssign from 'object.assign'
import { combineReducers } from 'redux'
import { namespaced } from 'redux-subspace'
import { ADD_DATA, ADD_SELECT, REMOVE_SELECT, SAVE_SELECT_OPTION } from './actions'

function AddDropdown(state = { selections: []}, action = {}){
  switch (action.type){
    case ADD_DATA:
      return ObjectAssign({}, state, action.data)
    case ADD_SELECT:
      return {
        ...state,
        selections: [].concat(state.selections, action.data),
      }
    case REMOVE_SELECT:
      return {
        ...state,
        selections: state.selections.filter((selection, index) => (index !== action.data)),
      }
    case SAVE_SELECT_OPTION:
      return {
        ...state,
        selections: state.selections.map((selection) => selection.id === action.data.id ? action.data : selection)
      }
    default:
      return state
  }
}


const FormApp = combineReducers({
  namespaced(AddDropdown, "nationality"),
  namespaced(AddDropdown, "countryOfResidence")
})

export default FormApp

NOTE: as per my comment there are some issues with this code and I have not attempted to clean them up for this example.