DEV Community

Cover image for Using Currying and Reducers in your Components
Jonathan Silvestri
Jonathan Silvestri

Posted on • Edited on

Using Currying and Reducers in your Components

Context

I came across some old code from a take home challenge. Part of the challenge was to create a form that could submit a name and an email. Here's how some of the code looks:

  const Form = () => {
    const [name, setName] = useState('')
    const [email, setEmail] = useState('')

    const reset = () => {
      setName('')
      setEmail('')
    }

    const handleSubmit = (event) => {
      event.preventDefault()
      apiCall({ name, email }).then(reset)
    }

    return <div>
     <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="name"
          onChange={event => setName(event.target.value)}
          value={name}
        />
         <input
          type="text"
          name="email"
          onChange={event => setEmail(event.target.value)}
          value={email}
        />
       <button type='submit'>Submit</button>
      </form>
      <button onClick={reset}>Reset Form</button>
     </div>
  }

Please excuse the lack of accessible inputs for this example.

Looking back at this code, it did exactly what I needed to do, but it wasn't easily extensible. If I had to track numerous fields with this form, where each input had its own state declaration, the component would get very large and become more and more bug prone.

Reducers to the rescue!

I'm a big fan of reducers (and useReducer) as they help to both organize the architecture for components and provide an API for when one state value relies on other state values.

In this example, the latter isn't the case as much, but the architecture piece is very important to this example. State reducers typically return your state and a dispatch helper that allows you to dispatch actions to update your state. Keeping all my state in one spot is incredibly beneficial as it greatly reduces the error rate and surface area of any future additions to state.

I suggest giving the React docs on useReducer a read if you haven't yet, as they will help understand my refactor of the above code:

  const INITIAL_STATE = {
    name: '',
    email: ''
  }

  const reducer = (state, action) => {
    switch(action.type) {
      case 'updateName':
       return { ...state, name: action.value }
      case 'updateEmail':
       return { ...state, email: action.email }
      case 'reset':
      default:
       return INITIAL_STATE
    }
  }

  const Form = () => {
    const [ state, dispatch ] = useReducer(reducer, INITIAL_STATE);
    const { name, email } = state

    const handleSubmit = (event) => {
      event.preventDefault()
      apiCall({ name, email }).then(() => dispatch({type: 'reset'}))
    }

    return <div>
     <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="name"
          onChange={event => dispatch({ type: 'updateName', value:  event.target.value)}
          value={name}
        />
         <input
          type="text"
          name="email"
          onChange={event => dispatch({ type: 'updateEmail', value: event.target.value)}
          value={email}
        />
       <button type='submit'>Submit</button>
      </form>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset Form</button>
     </div>

A little more code, but a much more standard API around how we update state. We've also introduced the ability to more easily consider loading states now, which we should be doing for any API calls that are involved. With a reducer that allows us to track and make updates to state based off of other state values, we have the architecture in place to make that sort of change. We'll leave that part alone, for now.

Introducing Currying to the mix

There's another piece that we can to this puzzle. We're going to leverage currying to further our code simplification.

Currying is the process where you take a function of 2+arity (arguments), and break it up into nested unary (single argument) functions. Each function will return a new function until the arguments are exhausted.

Simple math is the best way to illustrate what the above means. Let's implement a function that applies a modifier to some value, perhaps for price calculations.

  const priceMod = (price, markup) => price + (price * markup)

If I use this function in many places throughout my code, it'll get a bit repetitive, and it's likely I'll repeat myself a bunch:

  // In one file
  const tenPercentMarkup = priceMod(50, .1)
  // In another file
  const tenPercentMarkup = priceMod(50, .1)

Now, I could just make a file that has a tenPercentMarkup function exported, but that ends up being an abstraction that could be better represented with currying!

  const priceMod = markup => price => price + (price * markup)
  const tenPercentMarkup = priceMod(0.1)

Now that abstraction for the single 10% markup is inherent to priceMod thanks to the currying we've created!

  // Usage
  tenPercentMarkup(50)

Circling back to our Form

We can apply these concepts to the input fields we're updating in my Form:

const INITIAL_STATE = {
  name: "",
  email: ""
};

const reducer = (state, action) => {
  switch (action.type) {
    case "updateField":
      return { ...state, [action.field]: action.value };
    case "reset":
    default:
      return INITIAL_STATE;
  }
};

const Form = () => {
  const [state, dispatch] = React.useReducer(reducer, INITIAL_STATE);
  const { name, email } = state;

  const handleChange = field => event => {
    dispatch({
      type: "updateField",
      field,
      value: event.target.value
    });
  };

  return (
    <div className="App">
      <form>
        <input
          type="text"
          name="name"
          onChange={handleChange("name")}
          value={name}
        />
        <input
          type="text"
          name="email"
          onChange={handleChange("email")}
          value={email}
        />
        <button type="submit">Submit</button>
      </form>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

In order to keep my reducer lean, I'm leveraging computed property names to update the specific field value I'm editing. That way, updateField can handle any cases of inputs being changed.

The currying work happens in handleChange, where I am returning a function for each input field that mimics the setup of my original event handlers. With this function, I can create as many input fields as I need to without changing anything other than my INITIAL_STATE value!

Totally okay to not use the computed property names and have a case in the switch statement for each input field value as well, btw. I just like how updateField encapsulates the behavior I'm going for here.

Conclusion

I strongly suggest trying to look for this sort of pattern within your code. It'll probably help you to both uncover bugs and/or impossible states, as well as make your components more predictable and testable.

Top comments (0)