React-jsonschema-form: Bug with setState in event handlers

Created on 4 Sep 2018  路  8Comments  路  Source: rjsf-team/react-jsonschema-form

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

bug

Most helpful comment

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.

All 8 comments

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.

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.

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.

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.

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

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jabaren picture jabaren  路  3Comments

mfulton26 picture mfulton26  路  3Comments

FBurner picture FBurner  路  3Comments

Eric24 picture Eric24  路  3Comments

videni picture videni  路  3Comments