react-jsonschema-form icon indicating copy to clipboard operation
react-jsonschema-form copied to clipboard

Bug with setState in event handlers

Open MatejBransky opened this issue 6 years ago • 11 comments

Prerequisites

  • [x] I have read the documentation;
  • [x] In the case of a bug report, I understand that providing a SSCCE example is tremendously useful to the maintainers.

Description

There is an App which renders rjsf Form. When you call App's setState(updates value unrelated with the form's props) in Form.props.onChange then Form loses its internal state.

Steps to Reproduce

  1. Create a stateful component (A) which renders the Form with some simple schema.
  2. In this component (A) create onChange handler which is passed into the Form.
  3. In this handler call setState which updates some value which is not passed into the Form component.
  4. Type something into the input and you'll see that it immediately removes the typed value.

Here is a reproducible demo.

Expected behavior

Form shouldn't loose its state.

Actual behavior

Form lost its state.

Version

1.0.4

MatejBransky avatar Sep 04 '18 09:09 MatejBransky

Here is the new test which should fail:

import React from "react";
import { render, fireEvent } from "react-testing-library";
import Form from "../src";

it("should call provided change handler and keep the form state", () => {
  const schema = { title: "Foo", type: "string" };
  class App extends React.Component {
    state = { calls: 0 };

    handleChange = ({ formData }) => {
      this.setState(prevState => ({ calls: prevState.calls + 1 }));
    };

    render() {
      return (
        <Form
          schema={schema}
          onChange={this.handleChange}
          safeRenderCompletion={true}
        />
      );
    }
  }

  const { getByLabelText } = render(<App />);
  const input = getByLabelText("Foo");

  fireEvent.change(input, { target: { value: "bar" } });
  expect(input.value).toBe("bar"); // => fails, received: ""
});

It doesn't fail in CodeSandbox (bug?) but if you download the demo and try it on the local then you see that it fails.

MatejBransky avatar Sep 04 '18 10:09 MatejBransky

I've found the source of the issue. It's in the componentWillReceiveProps in the Form component. Everytime when you update a parent component then Form will reevaluate the internal state. You should check props used in the getStateFromProps and only after changes in these props you should call getStateFromProps.

I'm working with my fork (which uses templates from #1013 and it uses jest) but here is related commit.

MatejBransky avatar Sep 04 '18 12:09 MatejBransky

Now I'm not using cWRP (I've completely removed it) at all. It looks like that the prop key is the better way for uncontrolled components.

MatejBransky avatar Oct 01 '18 21:10 MatejBransky

Uh, it took me several hours to find this.

My current workaround: extend Form class and override componentWillReceiveProps to call the parent method only if it is really required.

Leksat avatar Jul 05 '19 06:07 Leksat

Any update on this? @Leksat can you please guide how can I follow your work around using hooks? @epicfaace any work around would be very appreciated. Thanks

affan-speridian avatar Aug 10 '20 12:08 affan-speridian

@affan-speridian

import Form from 'react-jsonschema-form';

class FixedForm extends Form {
  componentWillReceiveProps(nextProps) {
    const shouldSkipUpdate =
      Object.keys(nextProps).length === Object.keys(this.props).length &&
      Object.keys(nextProps).filter(
        key =>
          key !== 'children' &&

          // Maybe some other logic.

          nextProps[key] !== this.props[key]
      ).length === 0;
    if (shouldSkipUpdate) {
      return;
    }
    super.componentWillReceiveProps(nextProps);
  }
}

Leksat avatar Aug 10 '20 12:08 Leksat

@Leksat hey, thanks for responding. Much appreciated. I just tested it. It works but for a few seconds. I mean it preserves the internal state but as I go along filling the form. After few seconds older one got refreshed..

Here are my code:

form.js

import React from 'react'
import Form from 'react-jsonschema-form';

class FixedForm extends Form {

    constructor(props){
        super(props)
    }

    componentWillReceiveProps(nextProps) {
        const shouldSkipUpdate =
            Object.keys(nextProps).length === Object.keys(this.props).length &&
            Object.keys(nextProps).filter(
                key =>
                    nextProps[key] !== this.props[key]
            ).length === 0;
        if (shouldSkipUpdate) {
            return;
        }
        super.componentWillReceiveProps(nextProps);
    }

    render() {
        return <Form {...this.props}/>
    }
}

export default FixedForm

my custom-form.js in components folder

import React from 'react'
import RForm from './form'

export const Form = (props) => {
  return (
    <RForm {...props} widgets={{
      BaseInput: TextInput,
      CheckboxWidget,
      FileWidget: FileUpload,
      SelectWidget
    }}>
      <Spacer />
      <Button submit type="primary">{props.submitText}</Button>
      <Loader loading={props.loading} />
    </RForm>
  )
}

then in my component where I use it, I do

import { Form } from '../../custom-form'

<Form schema={schema} onSubmit={onSubmit} onChange={onChange} submitText="Add" noValidate={true} loading={loading} />

affan-speridian avatar Aug 10 '20 13:08 affan-speridian

Hi, I am having this issue too, is there any update on this bug already ? In the meantime I'll use this workarround :)

domercuri avatar Oct 29 '20 15:10 domercuri

#2010 looks relevant to this and might help with remediation.

Kos avatar Aug 03 '21 13:08 Kos

?This may actually be fixed in the v5 beta... Can someone please verify?

heath-freenome avatar Aug 30 '22 00:08 heath-freenome

Nop, still persists check https://codesandbox.io/s/sparkling-cdn-yrmhq5 Type something in the text input and click "OK".

MiguelFreire avatar Sep 14 '22 11:09 MiguelFreire

A much simpler workaround is to store the full form data and provide it on each render.

The code looks something like this:

const [formData, setFormData] = useState({});
const onChange = (e) => {
  setFormData(e.formData);
  // Set whatever else you need here.
};

return <Form
  onChange = {onChange}
  formData = {formData}
  // other properties omitted.
/>

mikolysz avatar Sep 27 '22 21:09 mikolysz

This issue has been automatically marked as possibly close because it has not had recent activity. It will be closed if no further activity occurs. Please leave a comment if this is still an issue for you. Thank you.

stale[bot] avatar Dec 28 '23 00:12 stale[bot]

This issue was closed because of lack of recent activity. Reopen if you still need assistance.

stale[bot] avatar Jan 27 '24 05:01 stale[bot]