Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sharing actions and reducers with react/redux

I'm still very much a react/redux noob. On one page I have a ton of text inputs. After a while I started noticing my action file has functions doing the same thing but for different inputs:

export function setInputName(string) {
  return {
    type: 'SET_CURRENT_NAME',
    payload: {value: string}
  };
}
export function setInputCity(string) {
  return {
    type: 'SET_CURRENT_CITY',
    payload: {value: string}
  };
}

With my reducer looking like:

export function currentName(state='', {type, payload}) {
  switch (type) {
  case 'SET_CURRENT_NAME':
    return payload.value;
  default:
    return state;
  }
}
export function currentCity(state='', {type, payload}) {
  switch (type) {
  case 'SET_CURRENT_CITY':
    return payload.value;
  default:
    return state;
  }
}

And my component had these multiple inputs:

import {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {setInputName, setInputCity} from 'actions/form';

export default class Form extends Component {
  static propTypes = {
    setInputName: PropTypes.func.isRequired,
    setInputCity: PropTypes.func.isRequired,
    currentName: PropTypes.string.isRequired,
    currentCity: PropTypes.string.isRequired
  }
  render() {
    let {setInputName, setInputCity, currentName, currentCity} = this.props;
    return (
      <div>
        <input
          type="text" 
          placeholder="Name"
          onChange={(e) => setInputName(e.currentTarget.value)}
          value={currentName}
        />
        <input
          type="text" 
          placeholder="City"
          onChange={(e) => setInputCity(e.currentTarget.value)}
          value={currentCity}
        />
      </div>
    );
  }
}

function select(state) {
  return {
    currentName: state.form.currentName,
    currentCity: state.form.currentCity
  };
}

function actions(dispatch) {
  return bindActionCreators({
   setInputName: setInputName,
   setInputCity: setInputCity,
  }, dispatch);
}

export default connect(select, actions)(Form);

Which isn't very DRY and I immediately thought I was doing something wrong. Is there a good way to have a common setInputValue action for all text inputs on every page and component of my app? I would also want to use a common reducer for every input. Thank you.

UPDATE

Here's my gross example that works but I feel like this is still a bit convoluted and there has to be a better way. Basically in the reducer I check to see if that input has been used and added to the state yet. If not I add it. If so I just update its value. EDIT I lied this doesn't actually work.

// actions
export function updateTextInput(name, value) {
  return {
    type: 'SET_CURRENT_TEXT_INPUT',
    payload: {name: name, value: value}
  };
}

// reducer
export function currentTextInput(state=[], {type, payload}) {
  switch (type) {
  case 'SET_CURRENT_TEXT_INPUT':
    let newState = state;
    let curInput = state.findIndex(function(elem) {
      return elem.name === payload.name;
    });
    if (curInput === -1) {
      newState.push({name: payload.name, value: payload.value});
    } else {
      newState[curInput].value = payload.value;
    }
    return newState;
  default:
    return state;
  }
}

// component
...
render() {
  let {updateTextInput, currentTextInput} = this.props;
  return (
    <div>
      <input
        type="text" 
        placeholder="Name"
        onChange={(e) => updateTextInput('name', e.currentTarget.value)}
        value={currentTextInput.name}
      />
      <input
        type="text" 
        placeholder="City"
        onChange={(e) => updateTextInput('city', e.currentTarget.value)}
        value={currentTextInput.city}
      />
    </div>
  );
}
...
like image 560
anthony-dandrea Avatar asked Nov 13 '15 20:11

anthony-dandrea


2 Answers

A small first step to simplify the code is to use higher order reducers/functions.

function makePropReducer(actionType, prop) {
  return (state = '', {type, payload}) => {
    if (type === actionType) {
      return payload[prop];
    }
    return state;
  };
}

export const currentName = makePropReducer('SET_CURRENT_NAME', 'value');
export const currentCity = makePropReducer('SET_CURRENT_CITY', 'value');
like image 145
Jacob Rask Avatar answered Oct 15 '22 02:10

Jacob Rask


Ok here's an example as requested. Let's say I have a User data model, it looks something like this:

{
  id: 1,
  name: 'Some Name',
  email: '[email protected]',
  age: 21
}

The way you currently have it set up you'd have to have SET_USER_NAME, SET_USER_EMAIL, SET_USER_AGE actions. No need for all those when you can have an UPDATE_USER action that receives an object of the updated values as an argument.

To set a new name for the first user you would call the UPDATE_USER action like:

updateUser(1, { name: 'New Name' })

In the Users reducer you'd update the users' state (an array of users) like:

function updateUser(usersState, id, attrs) {
  return usersState.map(user =>
    user.id === id ?
    Object.assign({}, user, attrs) :
    user
  );
}
like image 37
slightlytyler Avatar answered Oct 15 '22 04:10

slightlytyler