Tech

Using React Select with Redux Form

By April 22, 2019 May 17th, 2019 No Comments

At FireHydrant we use Redux Form for all of our forms. It is extremely easy to build complex form logic with all sorts of added bonuses that make using it in our React/Redux front end a no brainer. However, when we started using React Select for our select fields we started running into some issues. You are likely running into some of the same issues we did and so this blog post will help get you off the ground and integrating these two libraries together.

Managing React Select state with Redux Form

First things first, we need to set up React Select to communicate it's state and have it's value controlled by Redux Form. React Select needs to act as a controlled component with Redux Form acting as the state management tool that controls React Select. That means hooking into the props that React Select exposes and calling our Redux Form functions from there. Here is the very simple implementation to hook into React Select's onChange and onBlur props.

export const ReduxFormSelect = props => {
  const { input, options } = props;

  return (
    <Select 
      {...input} 
      onChange={value => input.onChange(value)} 
      onBlur={() => input.onBlur(input.value)} 
      options={options}
    />
  )
}

And calling this component looks something like this.

import React from 'react';
import { Field, reduxForm } from 'redux-form'
import ReduxFormSelect from './ReduxFormSelect'

const Form = props => {
  const { handleSubmit } = props;
  return (
    <form onSubmit={handleSubmit}>
      <Field name="currentUser" component={ReduxFormSelect} options={userOptions} />
    </form>
  )
}

export default reduxForm({ form: 'filter' })(Form);

A couple important things to note right off the bat. We still need to include all of the additional input props that Redux form gives us so we will destructure those props into the Select component. We also need to overwrite the onChange and onBlur functions provided by React Select. We will use the Redux Form onChange function so that the value emitted by React Select can be stored in our store by Redux Form. For onBlur, we want to set the value of the select to the input.value prop that Redux Form provides us.

Now you have React Select's state being managed by Redux Form! But wait, how do we populate the options of our select?

Creating options and setting Initial Values

When we used the Redux Form Field component in the last example it looked something like this.

<Field name="currentUser" component={ReduxFormSelect} options={userOptions} />

We were passing a userOptions variable to the select as an options prop so that it can be populated. But what does that userOptions variable look like? An array of option objects with label and value keys.

const userOptions = [
    {
      label: 'Erika',
      value: '4e4cf51f-b406-413a-ae46-2cf06c7aabff',
    },
    {
      label: 'Julia',
      value: 'edad97c7-f2dc-4198-91a9-8f20c7bc67b2',
    },
    {
      label: 'Sarah',
      value: '57d3578a-3583-4290-8bae-596a4da81a8d',
    },
  ];

An important thing to note here is that we have to explicitly pass objects with both label and value keys so React Select can build the options menu. If you don't provide those two keys your options will not be created. Setting an initial value to the select is also straightforward, from wherever you are creating your initial values object you can pass the same label value object as the value for the field. And if you're using a multi select you can pass an array of those objects. Lets set our currentUser field to initialize with Sarah.

export default reduxForm({
  form: 'filter',
  intialValues: {
    currentUser: {
      label: 'Sarah',
      value: '57d3578a-3583-4290-8bae-596a4da81a8d',
    },
  }
})(FilterForm);

There you go! Now your form will initialize with a correct initial value that is properly selected inside of React Select.

This should get you off the ground and controlling React Select through your redux store. We have run into some other things that have proven a little challenging and we will walk through them below.

Other Gotchas

Submitting React Select values

Since we are hooking directly into the onChange handler to update our state we are getting a slightly different output when compared to using a regular select. With a regular select, the value passed into your form handler will be the value attribute from the markup. React Select passes a little bit more information along:

{
  label: 'Sarah',
  value: '57d3578a-3583-4290-8bae-596a4da81a8d',
}

You might be tempted to change the onChange handler in our initial implementation to call onChange={value => input.onChange(value.value)} but React Select needs the entire object to be able to know what you have selected, it's the same as setting the initial values above. So in your handleSubmit function pull the value out of the object returned before submitting it.

const handleSubmit = values => {
  const newValues = Object.assign({}, values, {
    currentUser: values.currentUser.value
  })
  // The rest of your submission logic goes here
  fetch('/filter', {
    method: 'POST',
    body: JSON.stringify(newValues),
    headers:{
    'Content-Type': 'application/json'
    }
  })
}

This way React Select will track what option you have selected to apply the correct styles and display the selected options.

Populating initial values from an API response

We want to set our initial values based off of a request to a form state that we have saved in our database. Maybe that form state is saved in our redux state too. How can we set our initial values of the form to match our api response? Redux Form allows you to pass a prop called initialValues into your reduxForm HoC and it will use that object to create its initial values. We use this in mapStateToProps all the time

const formatUserForSelect = user => ({
  label: user.name,
  value: user.id,
});

const mapStateToProps = (state, ownProps) => {
  let initialValues = {};

  if (state.user) {
    initialValues.currentUser = formatUserForSelect(state.user);
  }

  return {
    initialValues,
  }
};

const reduxFilterForm = reduxForm({
  form: 'filter',
  enableReinitialize: true,
})(FilterForm);

export default connect(mapStateToProps)(reduxFilterForm);

If you are populating the form using an API response set enableReinitialize to true, without this Redux Form will not update your initial values after the component mounts even if they change. Also note that we aren't passing in an initial values key to the reduxForm HoC, Redux Form can accept initialValues as a prop from anywhere and will handle setting the initial values for you.

OnBlur handler breaks on mobile

With the current version of our onBlur handler we are checking the input.value prop to control the state of the select when the menu closes. Unfortunately React Select calls onChange and onBlur simultaneously on mobile. This leads to a race condition where the input prop has not updated by the time blur tries to check the value. By adding a timeout and a recheck on the props inside the timeout we can ensure that the onChange function has updated the store.

export const ReduxFormSelect = props => {
  const { input, options } = props;

  const handleBlur = () => {
    setTimeout(() => {
      const { input } = props;
      input.onBlur(input.value);
    }, 1);
  };

  return (
    <Select 
      {...input} 
      onChange={value => input.onChange(value)} 
      onBlur={handleBlur} 
      options={options}
    />
  )
}

Dispatching Changes

Another way to handle the changing fields of your form is to use Redux Form's actions and dispatch them yourself. We do this by using mapDispatchToProps to dispatch a change action.

import React from 'react';
import { Field, reduxForm, change } from 'redux-form';

export class FilterForm extends React.Component {
  state = { showAdvancedForm: false };
  componentDidUpdate = (prevProps, prevState) => {
    const { dispatchChange, initialFilterValues, initialValues } = this.props;
    const { showAdvancedForm } = this.state;

    if (showAdvancedForm && !prevState.showAdvancedForm) {
      dispatchChange('filter', 'date1', new Date().toISOString().slice(0, 10));
    }
  }

  render = () => {
    const { handleSubmit } = this.props;
    const { showAdvancedForm } = this.state;
    let advancedForm = '';

    if (showAdvancedForm) {
      advancedForm = (
        <React.Fragment>
          <div className="mb-3 mt-3">
            <Field name="date1" component={Input} type="date" label="Date" />
          </div>
        </React.Fragment>
      );

    }
    return (
      <Form onSubmit={handleSubmit}>
        <div className="mb-3">
          <Field name="query" component={Input} type="text" label="Search Query" />
        </div>
        {advancedForm}
        <div className="mb-3 text-right text-bottom">
          <Button type="button" className="btn btn-outline-primary" size="sm" onClick={this.toggleAdvanceForm}>Show Advanced</Button>
          <Button color="primary" size="sm" type="submit">Filter</Button>
        </div>
      </Form>
    );
}

const mapDispatchToProps = dispatch => ({
  dispatchChange: (formName, field, value) => dispatch(change(formName, field, value)),
});

const reduxFormFilterForm = reduxForm({
  form: 'filter',
})(FilterForm);

const connectFilterForm = connect(null, mapDispatchToProps)(reduxFormFilterForm);

export default connectFilterForm;

Here whenever we toggle the advanced form we dispatch a change action to prefill the date field to today.

I hope this was helpful, if you have any questions about how we've implemented this my Twitter handle is @nielsendylan.

FireHydrant takes you from oops to ops

Manage deploys, incidents, and post mortems like it's no big deal.

Learn More