Creating side effects with the onChange callback in Redux Form (TypeScript included!)

deckstar

Deckstar

Posted on October 16, 2020

Creating side effects with the onChange callback in Redux Form (TypeScript included!)

Redux Form is a common way to handle forms in React. But as with any large, complicated library, there are always a couple of features that may not seem obvious at first. In this article, we will take a look at how to use the onChange callback in the reduxForm({}) config.

The use case

This function is useful when you want to introduce some side effects to your forms. This side effect could be anything: maybe you want to fetch data for a dropdown. Or maybe you just want some values to change automatically.

In my case, I was developing a form for granting participation credits to students for showing up to experiments. Say you’re a professor: a student shows up to your experiment, so you mark him as “participated” and grant him, for example, 5 credits for his performance.

Student participated
A simple credit granting form. You’re a professor running an experiment, and your university’s budget department demands that you submit a form for each student saying how they participated, and how many credits they should get.

Here’s where the onChange comes in: another student didn’t show up, with neither an excuse nor a warning. So you mark him as a “no-show”. If a student is a no-show, you probably want him to get zero credits, right?

Now, wouldn’t it be nice if your Redux Form automatically chose zero credits for the student if you deemed them a “no-show”? That certainly sounds like better UX to me. But beyond UX, you may very well need such functionality in a case like this, where granting credit to a non-participant doesn’t make any sense, and probably breaks the rules of your university’s budget allocation.

Student no-show
Wouldn’t it be nice if selecting the big red X category automatically set the credits to zero?

Of course, you could just add some logic in your component, or in your Redux action, or in your backend. But it’s probably more convenient to have a simple solution that immediately solves the need for visual logic, Redux state logic, and the eventual form submission logic as well.

Enter the onChange callback: a simple solution to immediately handle the change according to your rules, leaving the rest of your component, reducers and backend none the wiser that a participant could’ve theoretically been in the big red X category and still gotten some points.

How to implement

Let’s say our form component looks like this:

import React, { Component } from 'react';
import { reduxForm } from 'redux-form';
// etc. sub-component imports

class ParticipantModal extends Component {
  render() {
    return (
      <Container>
        <Header />
        <ScrollView>
          <CreditStatusButtons />
          <CreditCount />
        </ScrollView>
        <SaveButton />
      </Container>
    )
  }
}

const ParticipantForm = reduxForm({
  form: "participantForm",
})(ParticipantModal)

export default ParticipantForm;
Enter fullscreen mode Exit fullscreen mode

We start out with a simple modal with some sub components (the code for which is not important for this illustration). The way Redux Form works is that our ParticipantModal component automatically gets access to an auto-generated object called “form”, which lives in our Redux state and includes pretty much everything needed to make a nice, reactive form. Redux Form takes care of most things, but for our “set credits to zero automatically” we will need to write a little custom logic.

We start out by writing out what we actually want to happen and when. So we write a handleOnFormChange function. Let’s say that we have two variables in this form: the studentStatus (participated, excused, no-show, etc.) and the creditValue (the number of credits to grant).

We can start out by adding an “onChange” field into our reduxForm config on the bottom. Then we declare a function that we want to be called when a form value (any value) changes. The config will automatically pass in four variables into this function, all of which we will need: newValues, dispatch, props and previousValues (in that order!).

const handleOnFormChange = (newValues, dispatch, props, previousValues) => {
  const { studentStatus: newStudentStatus } = newValues;
  const {
    studentStatus: prevStudentStatus,
    creditValue: prevCreditValue,
  } = previousValues;
  const { change: changeField } = props;

  /*
    if the user sets the participant as a "no show",
    then their credit value should be automatically set to zero
  */
  if (
    newStudentStatus !== prevStudentStatus && // to prevent dispatching every time
    newStudentStatus === 'noShow' &&
    prevCreditValue > 0
  ) {
    dispatch(changeField('creditValue', 0));
  }
};

const ParticipantForm = reduxForm({
  form: 'participantForm',
  onChange: handleOnFormChange, // <-- add this
})(ParticipantModal);
Enter fullscreen mode Exit fullscreen mode

newValues and previousValues are self-explanatory: they are the form values that were stored in the Redux state before and after the user changed something. Dispatch is the Redux dispatch function that is used with every Redux action & reducer. And props are the properties that reduxForm passes into your component, from which we destructure the change function. (I also rename it to changeField, to make it more obvious.) This function takes in the name of the value that we want to change (in our case, the creditValue), and the new value we want to set (zero). Make sure to check previous values so the dispatch is called only when you change the status!

And just like that, we’re done! With this little bit of logic, we have achieved the functionality that we wanted.

Adding TypeScript

This particular project required TypeScript. Though lately I’ve been becoming more and more of a TypeScript fan, one thing I never liked was spending a lot of time looking for interfaces / types for 3rd party libraries.

Well I’ve got you covered. Simply copy paste the type imports & uses below, and your linter should get rid of quite a few red lines. You’re going to need Dispatch from ‘react’ and DecoratedFormProps from ‘redux-form’.

import React, { Component, Dispatch } from 'react';
import { reduxForm, DecoratedFormProps } from 'redux-form';
Enter fullscreen mode Exit fullscreen mode

You’ll also need to declare your own interface for the values in your form.

interface YourForm {
  studentStatus: string;
  creditValue: number;
}
Enter fullscreen mode Exit fullscreen mode

Add them to your “handleOnFormChange” function.

With that, our final result should look something like this:

import React, { Component, Dispatch } from 'react';
import { reduxForm, DecoratedFormProps } from 'redux-form';
// etc. other component imports

interface YourForm {
  studentStatus: string;
  creditValue: number;
}

class ParticipantModal extends Component {
  render() {
    return (
      <Container>
        <Header />
        <ScrollView>
          <CreditStatusButtons />
          <CreditCount />
        </ScrollView>
        <SaveButton />
      </Container>
    );
  }
}

const handleOnFormChange = (
  newValues: YourForm,
  dispatch: Dispatch<any>,
  props: DecoratedFormProps<YourForm, {}, string>,
  previousValues: YourForm
) => {
  const { studentStatus: newStudentStatus } = newValues;
  const {
    studentStatus: prevStudentStatus,
    creditValue: prevCreditValue,
  } = previousValues;
  const { change: changeField } = props;

  /*
    if the user sets the participant as a "no show",
    then their credit value should be automatically set to zero
  */
  if (
    newStudentStatus !== prevStudentStatus && // to prevent dispatching every time
    newStudentStatus === 'noShow' &&
    prevCreditValue > 0
  ) {
    dispatch(changeField('creditValue', 0));
  }
};

const ParticipantForm = reduxForm({
  form: 'participantForm',
  onChange: handleOnFormChange,
})(ParticipantModal);

export default ParticipantForm;
Enter fullscreen mode Exit fullscreen mode

And that’s it! 🙂

P.S.: “But I’m still getting a TypeScript linter error?”

At the end of this, you may see a linter error at the bottom. Specifically, the ParticipantModal component that gets passed into reduxForm will have a warning that says something like Argument of type 'typeof ParticipantModal' is not assignable to parameter of type 'ComponentType<InjectedFormProps<FormValues, {}, string>>'.

I’ll be honest, I have no idea how to fix this linter error. I’ve tried and I’ve Googled it a dozen times, to no avail. I’ve just accepted that all my Redux Form components will have one TypeScript error on the bottom.

If you find a solution, would you please kindly share it with me? As a token of my gratitude, I promise I’ll send you the best picture of a cookie I can find 😇

Thanks for reading!

Edit on 2 December 2020: TypeScript linter error solved

Here's how to solve that TypeScript error above. Change the following lines as follows:

// import { reduxForm, DecoratedFormProps } from 'redux-form'; // old
import { reduxForm, DecoratedFormProps, InjectedFormProps } from 'redux-form';

// ...

// class ParticipantModal extends Component { // old
class ParticipantModal extends Component<Props, InjectedFormValues<YourForm, Props>> {
// Props is the interface with the normal, non-form-related props of this component 

// ...

  // props: DecoratedFormProps<YourForm, {}, string>, // old
  props: DecoratedFormProps<YourForm, Props, string>, 

// ...

// const ParticipantForm = reduxForm({ // old
const ParticipantForm = reduxForm<YourForm, Props>({ 


Enter fullscreen mode Exit fullscreen mode

Other lines shouldn't need any changes.

If your component has no Props, you can replace the "Props" interface with an empty object, e.g. class ParticipantModal extends Component<{}, InjectedFormValues<YourForm, {}>>.

Thanks again for reading, and good luck with your forms!

💖 💪 🙅 🚩
deckstar
Deckstar

Posted on October 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related