React-select: Usage in react-hook-form. Ref issue

Created on 14 Nov 2019  ยท  16Comments  ยท  Source: JedWatson/react-select

I'm trying to use react-select inside a react-hook-form and everything connects up nicely up until I want to change my value and submit the form.

The traditional react select box works fine but implementing the same in react-select looks like there's an issue with passing react-hook-form's register into ref.

Below is in example of a react-select field named "fruit_new" and the traditional select named "fruit_old"

The "fruit_new" field isn't being registered with the form

https://mfi92.csb.app/

Thanks!

issubug-unconfirmed

Most helpful comment

Thanks @chdorter for raising an issue here and trying out react-hook-form โค๏ธ as today, if you are using react-select, you will have to use custom register, which is what I have documented on the get-started (https://react-hook-form.com/get-started#WorkwithUIlibrary) page. You will have to leverage custom register, which is registering your input at useEffect and update value via setValue https://react-hook-form.com/api#setValue API.

@JedWatson Yea, that would be super nice โœŒ๐Ÿป to make it work with react-select out the box, there are quite a few questions people asking about using react-hook-form with react-select. I think it's achievable as well. react-hook-form embrace native form input API and uncontrolled form. so if we can read the input value via ref (hidden input) then we can solve the problem using setValue. (I am more than happy to contribute to making it work too. ) Another problem is reset(), which I need to do some investigate on how to detect form is been invoked with reset() API and clear react-select, so react-select can support form.reset() API. I think with react-hook-form gaining attraction, people start to realize that uncontrolled component is OK as well (with benefits). Again appreciated your time to looking into this ๐Ÿ™ (you were reading and replying this at 3 am... take care and thank you <3)

All 16 comments

So I think the problem is that react-hook-form expects form controls to implement imperative APIs that react-select doesn't conform to. Happy to explore further with @bluebill1049 whether supporting using both libraries together is possible, and which changes to either project may be required, but out of the box right now I don't think they're compatible.

Thanks @chdorter for raising an issue here and trying out react-hook-form โค๏ธ as today, if you are using react-select, you will have to use custom register, which is what I have documented on the get-started (https://react-hook-form.com/get-started#WorkwithUIlibrary) page. You will have to leverage custom register, which is registering your input at useEffect and update value via setValue https://react-hook-form.com/api#setValue API.

@JedWatson Yea, that would be super nice โœŒ๐Ÿป to make it work with react-select out the box, there are quite a few questions people asking about using react-hook-form with react-select. I think it's achievable as well. react-hook-form embrace native form input API and uncontrolled form. so if we can read the input value via ref (hidden input) then we can solve the problem using setValue. (I am more than happy to contribute to making it work too. ) Another problem is reset(), which I need to do some investigate on how to detect form is been invoked with reset() API and clear react-select, so react-select can support form.reset() API. I think with react-hook-form gaining attraction, people start to realize that uncontrolled component is OK as well (with benefits). Again appreciated your time to looking into this ๐Ÿ™ (you were reading and replying this at 3 am... take care and thank you <3)

you were reading and replying this at 3 am... take care and thank you <3

You're welcome ๐Ÿ™‚

I think to make it work out of the box, I'm not sure the best place for this code to go but it sounds like we could create a wrapper for react-select that handles ensuring it conforms to the react-hook-form API. Something like this:

import Select from 'react-select';

export class ReactHookSelect extends Component {
  constructor({ value }) {
    this.state = { value };
    Object.defineProperty(this, 'value', {
      set: function(value) {
        this.setState({ value });
      },
      get: function() {
        return this.state.value;
      }
    });
  }
  handleChange = (value, eventMeta) => {
    if (this.props.onChange) {
      this.props.onChange(value, eventMeta);
    }
    this.setState({ value });
  }
  handleRef = (ref) => {
    this.select = ref;
  }
  reset() {
    this.setState({ value: null });
  }
  focus() {
    this.select.focus();
  }
  blur() {
    this.select.blur();
  }
  render() {
    const { value } = this.state;
    return <Select {...this.props} value={value} onChange={this.handleChange} ref={this.handleRef} />
  }
}

That doesn't solve form.reset() yet but gets us pretty close I think?

That doesn't solve form.reset() yet but gets us pretty close I think?

WOW this is such a great idea~! I never thought about creating a wrapper around! thanks, @JedWatson I think I can create a generic component for this.

import Select from 'react-select';
import useForm from 'react-hook-form';
import HookFormInput from 'react-hook-form-input';

<HookFormInput component={Select} {...props} ref={register} />

Need to figure out a good name for that generic component. This is going to solve a lot of problems! You are super smart โค๏ธโœจ going to give a try tonight or over the weekend :)

OK working version based on @JedWatson 's wonderful idea!

https://codesandbox.io/s/goofy-flower-rzu9s

import * as React from "react";

export default props => {
  const [value, setValue] = React.useState();
  const valueRef = React.useRef();
  const handleChange = value => {
    props.setValue(props.name, value);
    valueRef.current = value;
  };

  React.useEffect(() => {
    props.register(
      Object.defineProperty(
        {
          name: props.name,
          type: "custom"
        },
        "value",
        {
          set(value) {
            setValue(value);
            valueRef.current = value;
          },
          get() {
            return valueRef.current;
          }
        }
      )
    );
  }, [props, value]);

  return React.createElement(props.children, {
    ...props,
    onChange: handleChange,
    value
  });
};

usage:

const { handleSubmit, register, setValue } = methods;
<HookFormInput children={Select} options={options} name="test1" {...methods} />

You fellas have gone above and beyond expectations! Much appreciate the time and depth you've put into your responses.
I ended up doing what @bluebill1049 initially suggested, which was a very easy implementation, so you guys going the extra mile to make it even easier is just awesome.

The combination of these two, TS and hooks works extremely well so keep up the amazing work!

Hey @chdorter, I have been working on this new wrapper component last night. https://github.com/react-hook-form/react-hook-form-input Going to polish it up over the weekend, we can close the gap between controlled component with react hook form.

@chdorter how did you go with this issue? I think RHFInput should solve your problem.
@JedWatson i think we can close this issue. ๐Ÿ‘

@bluebill1049 Gave it a quick test locally with react-select and works super neat out of the box.
For now, I will continue to use my current implementation based on your initial suggestion as the RHFInput setValue uses the react-select {label,value} object and my RHF Forms TS objects currently expect just value (which I am setting using setValue on the onChange event of react-select).
I'll do a refactor in the coming weeks and I'm also using a few AntD components so will give that a test.

Again, much appreciate the effort you both have gone to in investigating and implementing a solution.

awesome thanks @chdorter for the update! <3

@JedWatson @bluebill1049 awesome, I managed to have react-select working with react-hook-form! ๐ŸŽ‰

Question: is it possible to reshape the value of the controlled components flowing in/out the form values?

My typical use case:

  • I have "user" model with a "topics" field which is an array of [1,2,3]
  • ProfileForm component starts up and user is fetched from api
  • form is 1:1 with user entity and form is initialized with user values where there's the field:
{
   "email": "blabla",
   "first_name": "blabla", 
   "topics": [1,2,3] 
}
  • topics is a react-select with multiple enabled

What happens?

  • since hook is passing [1,2,3] to react-select, it doesn't correctly set initial values because it expects a complex object like {label:"name",value:1}
  • since react-select sets an array of [{label:"name",value:1}], the submit doesn't have the expected shape of [1,2,3]

I hope it was clear. But it's something I typically have to deal with with extra logic in/out.
Do you have a nicer way to work this around?

@damianobarbati one way I can think of doing it is building a wrapper on top of react-select, so your data manipulation doesn't reflect what's required for react-select while still allow you to transform the selected value.

I was trying something like that. Here what I did so far:

// init form
const { register, control, handleSubmit, errors, watch } = useForm({
    mode: 'onBlur',
    reValidateMode: 'onChange',
    nativeValidation: false,
    defaultValues: {
        // topics: [{ label: '#food', value: 1 }], <= this works
        // topics: [1, 2], <= this does not work
    },
});

// later in the render, please note I have to give defaultValue here for my custom select to work
<SelectMulti name={'topics'} control={control} options={{ 1: '#food', 2: '#fitness', 3: '#fashion', 4: '#tech', 5: '#travel', 6: '#music', 7: '#video' }} defaultValue={[2]} />

Custom select:

// you can pass defaultValue=[1,2,3]
const SelectMulti = ({ label, name, options, control, defaultValue, ...props }) => {
    // convert options from {a:1} to [{label=a,value=1}]
    const optionsComputed = [];
    for (const [value, label] of Object.entries(options))
        optionsComputed.push({ label, value });

    let defaultValueComputed;

    // convert defaultValue from [1] to [{label=a,value=1}]
    if (defaultValue) {
        defaultValueComputed = [];
        for (const value of defaultValue)
            defaultValueComputed.push(optionsComputed.find(option => option.value == value));
    }

    return (
        <label>
            <span>{label}</span>

            <Controller
                name={name}
                defaultValue={defaultValueComputed}
                as={Select}
                options={optionsComputed}
                isMulti={true}
                isClearable={true}
                control={control}
                {...props}
            />
        </label>
    );
};

Problem is:

  • I can't use the hook useForm({defaultValues}), I don't know where in the component I can intercept those values to simulate the same defaultValue logic
  • I can't change the onChange prop on Controller to alter the returned value of the react-select stops working

Any idea? ๐Ÿค”

@damianobarbati have you seen this example: https://codesandbox.io/s/react-hook-form-controller-079xx

Sure! I started right from there and thanks for it @bluebill1049, it saved my made day.

Here a simplified fork just with the react-select: https://codesandbox.io/s/react-hook-form-controller-71v6w?file=/src/index.js

I'm doing an extra mile trying to have leaner data-struct in/out of form.
If only I could hide that {label,value} logic of react-select inside a component then I could only deal with plain objects and arrays with my forms.

EDIT:
This is the line I'm trying to have working https://codesandbox.io/s/react-hook-form-controller-71v6w?file=/src/index.js:265-286

EDIT:
I actually have another use-case: I typically have dates fetched in ISO8601 format.
I can't pass that value as defaultValue down to my Date inputs, because it expects yyyy-mm-dd.
If I knew how then I could format the defaultValue as soon as my date input receives it, without making manipulating the "defaultValues" config.
Just documenting use cases here, I'll make a complete codesandbox about this.

Hi all,

Thank you everyone who had a part in addressing this question!

In an effort to sustain the react-select project going forward, we're closing issues that appear to have been resolved via community comments.

However, if you feel this issue is still relevant and you'd like us to review it, or have any suggestions regarding this going forward - please leave a comment and we'll do our best to get back to you!

Was this page helpful?
0 / 5 - 0 ratings