Search code examples
reactjstypescriptimmer.js

'TypeError: illegal operation attempted on a revoked proxy' when using Immer with setState


I recently refactored my React App to use Immer. However, in the onFormValueChange using produce gives me the error TypeError: illegal operation attempted on a revoked proxy while the version that is written without produce works fine.

This is the smallest I could reduce the relevant code to :


test.tsx

import { produce } from 'immer';
import { IFormValues, TestForm } from './test_form';

interface ITestProps {}

interface ITestState {
  type: string;
}

export class Test extends React.Component<ITestProps, ITestState> {
  constructor(props: ITestProps) {
    super(props);

    this.state = {
      type: '',
    };
  }

  handleSubmit = (values: IFormValues) => {
    console.log.log(values);
  };

  onFormValueChange = (values: IFormValues) => {
    this.setState(
      produce((draft: ITestState) => {
        draft.type = values.type;
      }),
    );
  };

  // The Following version of the function works perfectly fine and as expected:
  //
  // onFormValueChange = (values: IFormValues) => {
  //   this.setState({
  //     type: values.type,
  //   });
  // };

  render() {
    let showField = true;
    if (this.state.type === 'test') {
      showField = false;
    }
    return (
      <div>
        <TestForm
          submit={this.handleSubmit}
          onValueChange={this.onFormValueChange}
        >
          <input name="type" />
        </TestForm>

        {showField && this.state.type}
      </div>
    );
  }
}

test_form.tsx

import produce from 'immer';

export interface IFormValues {
  [key: string]: any;
}

interface IFormProps {
  submit: (values: IFormValues) => void;
  onValueChange?: (values: IFormValues) => void;
}

export interface IFormState {
  values: IFormValues;
}

interface IFieldProps {
  value: any;
  name: string;
  onChange: (event: any) => void;
}

export class TestForm extends React.Component<IFormProps, IFormState> {
  constructor(props: IFormProps) {
    super(props);

    const values: IFormValues = {};

    this.state = {
      values,
    };
  }

  private handleSubmit = (event: any) => {
    event.preventDefault();

    const { submit } = this.props;
    const { values } = this.state;

    submit(values);
  };

  handleChange = (event: any) => {
    const { name, value } = event.target;

    this.setState(
      produce((draft: IFormState) => {
        draft.values[name] = value;

        this.props.onValueChange && this.props.onValueChange(draft.values);
      }),
    );
  };

  public render() {
    const { values } = this.state;
    return (
      <form onSubmit={this.handleSubmit} noValidate={true}>
        <div>
          {React.Children.map(
            this.props.children,
            (child: React.ReactElement<IFieldProps>) => (
              <div>
                {React.cloneElement(child, {
                  value: values[child.props.name],
                  onChange: this.handleChange,
                })}
              </div>
            ),
          )}
          <div>
            <button type="submit">Submit</button>
          </div>
        </div>
      </form>
    );
  }
}

Solution

  • Caveat: I've never used Immer. But the error is quite clear: You're trying to use a revoked Proxy. My guess is the fundamental problem is here:

    this.setState(
      produce((draft: IFormState) => {
        draft.values[name] = value;
    
        this.props.onValueChange && this.props.onValueChange(draft.values); // <=======
      }),
    );
    

    In produce, you're passing draft.values into a function that will call produce a second time and put values.type on a different draft state. My guess is you're not allowed to pass data out of the original produce call in that way. (The documentation says "Warning: please note that the draft shouldn't be 'leaked' from the async process and stored else where. The draft will still be revoked as soon as the async process completes." but that warning is in relation to async producers, and yours aren't async. Still, it may be that it's a more general warning, it just happens to be in the async producers part.)

    If so, this change to handleChange in TestForm would fix it:

    this.setState(
      produce((draft: IFormState) => {
        draft.values[name] = value;
      }),
      () => {
        this.props.onValueChange && this.props.onValueChange(this.state.values);
      }
    );
    

    That ensures that it calls onValueChange with the value after the state has been set (presumably it's a normal object at that point, not a proxy).