rjsf-team / react-jsonschema-form

A React component for building Web forms from JSON Schema.
https://rjsf-team.github.io/react-jsonschema-form/
Apache License 2.0
14.34k stars 2.19k forks source link

Using a custom widget causes re-render and loses focus #4299

Open macintushar opened 1 month ago

macintushar commented 1 month ago

Prerequisites

What theme are you using?

chakra-ui

Version

5.x

Current Behavior

I have added a custom widget. When I type in it in the form, it re-renders the field, which causes it to lose focus on the input field and moves the cursor's current position to the beginning of the input field. Note: If autofocus is not present in the input props, it completely loses focus and I can't type at all.

https://github.com/user-attachments/assets/aebf8d33-fb7a-4f80-ac65-e0707aeaf7e2

Expected Behavior

Ideally, it should work the same way the default input function works. It shouldn't lose focus and shouldn't rerender.

Steps To Reproduce

  1. Register a Custom Widget
  2. Use custom widget in a form

Environment

- OS: macOS Sonoma
- Node: v22.3.0
- npm: 10.8.1

Anything else?

I followed the code written in the docs here https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-widgets-fields

heath-freenome commented 1 month ago

@macintushar Without looking at the code of your widget we can't help you. I'm guessing that having the field rerender itself is the cause of the issue. React is really picky about how rerenders work especially around focus/blur behaviors and how callbacks work. I would focus on that first

macintushar commented 1 month ago

Hey @heath-freenome, this is the code i'm using. It's from the docs.

const MyCustomWidget = (props: WidgetProps) => {
    return (
      <input
        type='text'
        className='custom'
        value={props.value}
        required={props.required}
        onChange={(event) => props.onChange(event.target.value)}
      />
    );
  };

  const widgets: RegistryWidgetsType = {
    customWidget: MyCustomWidget,
  };

https://github.com/user-attachments/assets/2d6cc555-c00c-40c1-ab3e-45d5cdaff260

macintushar commented 1 month ago

Hey @heath-freenome, did you get some time to check out the issue? ^

macintushar commented 1 month ago

Hey @heath-freenome, could you please check this out? ^

heath-freenome commented 1 month ago

React can be picky about rendering when props change. The onChange is not a callback function so that might be the issue. How does this work?:

const MyCustomWidget = (props: WidgetProps) => {
    const { onChange, value, required } = props;
   const handler = React.useCallback((event) => onChange(event.target.value));
    return (
      <input
        type='text'
        className='custom'
        value={value}
        required={required}
        onChange={handler}
      />
    );
  };

  const widgets: RegistryWidgetsType = {
    customWidget: MyCustomWidget,
  };
macintushar commented 1 month ago

Hey @heath-freenome, I tested out the code you gave me with some minor changes to make it work properly (adding deps to useCallback), it seems to be working fine only if I use autoFocus as a prop. Please check out the two videos below to see what I mean:

Code v1:

  const MyCustomWidget = (props: WidgetProps) => {
    const { onChange, value, required } = props;
    const handler = useCallback((event) => onChange(event.target.value), [onChange]);
    return (
      <>
        <label>Custom Widget</label>
        <input
          type='text'
          className='custom'
          value={value}
          required={required}
          onChange={handler}
        />
      </>
    );
  };

Output for v1

https://github.com/user-attachments/assets/1e89a658-cc94-4c35-9bc1-3e80ffeed601

Code for v2

  const MyCustomWidget = (props: WidgetProps) => {
    const { onChange, value, required } = props;
    const handler = useCallback((event) => onChange(event.target.value), [onChange]);
    return (
      <>
        <label>Custom Widget</label>
        <input
          type='text'
          className='custom'
          value={value}
          required={required}
          onChange={handler}
          autoFocus
        />
      </>
    );
  };

Output for v2

https://github.com/user-attachments/assets/ec441580-bcd6-4005-888b-eb5edb7b2d86

The diff between the two is only the autoFocus prop. It seems to loose focus anytime there is a change and the only reason it's working is because of the autoFocus. Is this expected behaviour?

Is there a way to get this to work without using the autoFocus prop?

macintushar commented 1 month ago

@heath-freenome noticed another issue.

If i use a Textarea instead of an input, it results in some pretty weird behavior. It sends my cursor to the first character position every time i type. Please check out the video below

https://github.com/user-attachments/assets/43f4ebbc-ab7c-4ef5-b50e-bb0090dc4c87

Code:

const MyCustomWidget = (props: WidgetProps) => {
    const { onChange, value, required } = props;
    const handler = useCallback(
      (event: { target: { value: any } }) => onChange(event.target.value),
      [onChange],
    );
    return (
      <>
        <label>Custom Widget</label>
        <textarea
          className='custom'
          value={value}
          required={required}
          onChange={handler}
          autoFocus
        />
      </>
    );
  };
heath-freenome commented 1 month ago

@macintushar Everything you are describing lends me to believe that something in how you are wrapping the Form is breaking the rules of React and causing the focus to be disrupted. I really do not have a lot of resources to help you debug this. Do you have a way to provide a reproducible test case using codesandbox.io?

macintushar commented 1 month ago

@heath-freenome this is for something on the Multiwoven platform. It is open source but it's difficult for me to run this on a codesanbox. I'm not sure how i'd be able to show you this occurring without me getting on a call or something with you. You can run our code locally to try and emulate my steps if required.

macintushar commented 1 month ago

I don't see any issues in the console w.r.t. React or me breaking the Rules of React with my code change. Please let me know how we can proceed further.

macintushar commented 1 month ago

@heath-freenome All I see is this in the console which seems related to RJSF. The ID error was due to something I was testing but the other two still remain.

https://github.com/user-attachments/assets/32b1f62f-a552-4d34-a456-bad813fafdaf

heath-freenome commented 1 month ago

@macintushar without a reproducible test case I'm not sure how to help you. Why do you need the custom widget anyway? From your demo it doesn't like the code does anything special. If you are just needing to inject an onChange handler or tweak props, then rather than build a whole component, you can wrap one of the OOTB chakra-ui widgets using this documentation

nickgros commented 1 month ago

@macintushar another wild guess is that you might be creating your widget inside of a component, which is re-created on each render. But like @heath-freenome said, without something like a CodeSandbox, we can't do much