Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage in react-hook-form. Ref issue #3855

Closed
chdorter opened this issue Nov 14, 2019 · 19 comments
Closed

Usage in react-hook-form. Ref issue #3855

chdorter opened this issue Nov 14, 2019 · 19 comments
Labels
issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet

Comments

@chdorter
Copy link

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!

@JedWatson
Copy link
Owner

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.

@bluebill1049
Copy link

bluebill1049 commented Nov 20, 2019

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)

@JedWatson
Copy link
Owner

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?

@bluebill1049
Copy link

bluebill1049 commented Nov 21, 2019

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 :)

@bluebill1049
Copy link

bluebill1049 commented Nov 21, 2019

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} />

@chdorter
Copy link
Author

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!

@bluebill1049
Copy link

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.

@bluebill1049
Copy link

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

@chdorter
Copy link
Author

chdorter commented Dec 18, 2019

@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.

@bluebill1049
Copy link

awesome thanks @chdorter for the update! <3

@damianobarbati
Copy link

@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?

@bluebill1049
Copy link

@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.

@damianobarbati
Copy link

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? 🤔

@bluebill1049
Copy link

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

@damianobarbati
Copy link

damianobarbati commented Apr 24, 2020

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.

@bladey bladey added the issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet label Jun 3, 2020
@bladey
Copy link
Contributor

bladey commented Jun 5, 2020

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!

@bladey bladey closed this as completed Jun 5, 2020
@adams-family
Copy link

Hi @bladey and @JedWatson ,

I'd like to suggest reopening because this issue is still relevant. An attempt to use react-select with react-hook-forms still results in a runtime error when an item is selected:

import { useForm } from 'react-hook-form';
import Select from 'react-select'

function Component() {
  const { register } = useForm();

  return (
   <Select {...register('country')} />
  );

Throws the following error:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'name')
    at onChange (createFormControl.ts:655:1)
    at useStateManager-7e1e8489.esm.js:36:1
    at Select._this.onChange (RequiredInput.tsx:14:1)
    at Select._this.setValue (RequiredInput.tsx:14:1)
    at Select._this.selectOption (RequiredInput.tsx:14:1)
    at onSelect (RequiredInput.tsx:14:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)
    at invokeGuardedCallback (react-dom.development.js:4277:1)
    at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4291:1)

I suggest that we should have a look at this so that react-select can work out of the box with react-hook-form using the register function.

@bluebill1049
Copy link

Please use Controller, react-select is a controlled component. you can find an example in the doc as well:

https://codesandbox.io/s/react-hook-form-v7-controller-5h1q5

@adams-family
Copy link

@bluebill1049 I managed to get it work. It considerably more boilerplate than the register function but I believe that if it was possible to do it in a more straightforward way you would have done so.

The only issue with the following code (trying to be minimalistic) is that the onChange event on the Controller which is never triggered. Am I doing something wrong?

<Controller
  control={control}
  name="country"
  onChange={onCountryChange}      /*  << ----- This one does not get called ----- */
  render={({ field: { onChange, value, ref }}) => (
    <Select
      inputRef={ref}
      value={options(c => { return value == c.value; })}
      onChange={val => onChange(val.value)}
      options={options}
    />
  )}
/>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet
Projects
None yet
Development

No branches or pull requests

6 participants