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

[BUG] Custom component unmounting #485

Open
1 of 2 tasks
FractalStranger opened this issue Nov 15, 2022 · 7 comments
Open
1 of 2 tasks

[BUG] Custom component unmounting #485

FractalStranger opened this issue Nov 15, 2022 · 7 comments
Assignees
Labels

Comments

@FractalStranger
Copy link

FractalStranger commented Nov 15, 2022

Hi,

I have a React18 project and wanted to make custom react-formio component (code below) but this.container.unmount in detachReact method shows this warning:

Warning: Attempted to synchronously unmount a root while React was already rendering. React cannot finish unmounting the root until the current render has completed, which may lead to a race condition.

is there any way to unmount the element without the warning? (And is react-formio compatible with React 18 overall?)

Thanks.

Code:

export default class Grid extends ReactComponent {
  ...
  ...

  attachReact(element) {
    this.container = createRoot(element)
    return this.container.render(
      <HistoryRouter history={history}>
        <GridComponent {...{ ...this }} />
      </HistoryRouter>
    )
  }

  detachReact(element) {
    if (element) {
      this.container.unmount() // this creates the race condition warning
    }
  }
}

Environment

  • Hosting type
    • Form.io
    • Local deployment
      • Version:
  • Formio.js version: 5.0.0-m.3
  • Frontend framework: React 18
@aryanisml
Copy link

Do u have any example of custom component with react typescript if yes please share.

@FractalStranger
Copy link
Author

Typescript? Does react-formio support Typescript? It doesn't seem so.

Here is the code of custom component:
https://jsfiddle.net/kv0tq9p8/

@govind15496
Copy link

Hey I am a newcomer in this project and I would like to contribute

@mtruon11
Copy link

mtruon11 commented Jul 31, 2023

Hi,

I have a React18 project and wanted to make custom react-formio component (code below) but this.container.unmount in detachReact method shows this warning:

Warning: Attempted to synchronously unmount a root while React was already rendering. React cannot finish unmounting the root until the current render has completed, which may lead to a race condition.

is there any way to unmount the element without the warning? (And is react-formio compatible with React 18 overall?)

Thanks.

Code:

export default class Grid extends ReactComponent {
  ...
  ...

  attachReact(element) {
    this.container = createRoot(element)
    return this.container.render(
      <HistoryRouter history={history}>
        <GridComponent {...{ ...this }} />
      </HistoryRouter>
    )
  }

  detachReact(element) {
    if (element) {
      this.container.unmount() // this creates the race condition warning
    }
  }
}

Environment

  • Hosting type

    • Form.io

    • Local deployment

      • Version:
  • Formio.js version: 5.0.0-m.3

  • Frontend framework: React 18

I wonder if you can share your GridComponent code? I am trying to implement it myself but I am not sure how to start.

@lotorvik
Copy link

@FractalStranger did you figure any way to get around this?

@YoungDriverOfTech
Copy link

@FractalStranger
Hello, did you find someway to figure this out yet? Same issue I also encoutered.

@brendanbond
Copy link
Contributor

brendanbond commented Aug 6, 2024

Hey @YoungDriverOfTech @lotorvik @FractalStranger thanks for chiming in here. A couple of things to consider:

  1. I would consider this functionality (in which a react rendering context/instance is "embedded" into a @formio/js render cycle) to really be experimental; my guess is that we're not going to fully support this at any point in the near future. It requires a pretty in-depth knowledge of form.io internals at the moment - for example, we use the "ref" attribute for different reasons than React does so it becomes necessary to change the name of our "ref" attribute so React doesn't complain - and I'm not aware of any plans to clean up the APIs so that something like this would be more easily possible.
  2. We've made some updates in the development branch (in our org, that is master) that refactor the @formio/react library for Typescript and React 18 support. They'll be released with the 5.x version of @formio/js, which is not that far off but still is in development.
  3. I've done some of my own experimenting with this, and here's a file that shows how one might accomplish something like this in React 18 using some of the new changes to @formio/react (and the beta 5.x version of the @formio/js renderer). Keep in mind that there are no guarantees here (see above) and that this is all just experimentation.
import React from 'react';
import { Components } from '@formio/js';
import { Root, createRoot } from 'react-dom/client';
import { TextFieldWrapper } from './TextFieldWrapper';

type JSON = string | number | boolean | null | { [x: string]: JSON } | JSON[];

const Field = Components.components.field;
export class MUITextField extends Field {
    reactRendered: boolean;
    reactRoot: Root | null;
    /**
     * This is the first phase of component building where the component is instantiated.
     *
     * @param component - The component definition created from the settings form.
     * @param options - Any options passed into the renderer.
     * @param data - The submission data object.
     */
    constructor(component: any, options: any, data: any) {
        super(component, options, data);
        this.reactRendered = false;
        this.reactRoot = null;
        this._referenceAttributeName = 'data-formioref';
    }

    /**
     * This method is called any time the component needs to be rebuilt. It is most frequently used to listen to other
     * components using the this.on() function.
     */
    init() {
        return super.init();
    }

    /**
     * This method is called before the component is going to be destroyed, which is when the component instance is
     * destroyed. This is different from detach which is when the component instance still exists but the dom instance is
     * removed.
     */
    destroy() {
        this.detachReact();
        return super.destroy();
    }

    /**
     * This method is called before a form is submitted.
     * It is used to perform any necessary actions or checks before the form data is sent.
     *
     */
    beforeSubmit() {
        return super.beforeSubmit();
    }

    /**
     * The second phase of component building where the component is rendered as an HTML string.
     *
     * @returns {string} - Returns the full string template of the component
     */
    render(): string {
        // For React components, we simply render a div which will become the React root.
        // By calling super.render(string) it will wrap the component with the needed wrappers to make it a full component.
        return super.render(`<div data-formioref="react-${this.id}"></div>`);
    }

    /**
     * The third phase of component building where the component has been attached to the DOM as 'element' and is ready
     * to have its javascript events attached.
     *
     * @param element
     * @returns {Promise<void>} - Return a promise that resolves when the attach is complete.
     */
    attach(element: HTMLElement): Promise<void> {
        // The loadRefs function will find all dom elements that have the "ref" setting that match the object property.
        // It can load a single element or multiple elements with the same ref.
        this.loadRefs(
            element,
            {
                [`react-${this.id}`]: 'single',
            },
            'data-formioref',
        );

        if (this.refs[`react-${this.id}`]) {
            this.element = this.refs[`react-${this.id}`];
            this.attachReact(this.element);
        }
        return new Promise<void>((resolve) => {
            // Wait for the react root to be fully rendered before continuing the attach phase
            this.on(`react-rendered-${this.id}`, () => {
                super.attach(element);
                resolve();
            });
        });
    }

    /**
     * The fourth phase of component building where the component is being removed from the page. This could be a redraw
     * or it is being removed from the form.
     */
    detach() {
        if (this.refs[`react-${this.id}`]) {
            this.detachReact();
        }
        super.detach();
    }

    /**
     * Callback passed to our React root (via the wrapper's useEffect) that will signal that React has finished rendering.
     */
    onReactRendered = () => {
        // verify that our Form.io-specific reference attribute is present
        this.loadRefs(
            this.element,
            {
                input: 'multiple',
            },
            'data-formioref',
        );
        if (
            !this.refs['input'] ||
            (Array.isArray(this.refs['input']) &&
                this.refs['input'].length === 0)
        ) {
            console.warn(
                "Can't find an associated input element. Has React finished rendering? Does your input element have a formio-specific ref attribute?",
            );
            return;
        }
        this.reactRendered = true;
        this.emit(`react-rendered-${this.id}`, {});
    };

    /**
     * Attaches React to the previously rendered <div /> element. The TextFieldWrapper component
     * is passed our onReactRendered callback so that we can signal when React has finished rendering.
     *
     * @param element
     *
     */
    attachReact(element: HTMLElement) {
        console.log('attaching react...');
        if (!this.reactRoot) {
            this.reactRoot = createRoot(element);
        }
        this.reactRoot.render(
            <TextFieldWrapper
                onRendered={this.onReactRendered.bind(this)}
                onChange={(event) => {
                    this.setValue(event.target.value);
                }}
                variant={'filled'}
                fullWidth
                label={this.component.label}
                inputProps={{ 'data-formioref': 'input' }}
            />,
        );
    }

    detachReact() {
        console.log('detaching react...');
        if (this.element && this.reactRoot) {
            this.reactRoot.unmount();
        }
    }

    /**
     * Something external has set a value and our component needs to be updated to reflect that. For example, loading a submission.
     *
     * @param value
     */
    setValue(value = '') {
        if (!this.reactRendered) {
            this.on(`react-rendered-${this.id}`, () => {
                super.setValue(value, { modified: true });
            });
            return false;
        }
        super.setValue(value, { modifed: true });
        return true;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants