[BUG] Custom component unmounting
FractalStranger opened this issue · comments
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
Do u have any example of custom component with react typescript if yes please share.
Typescript? Does react-formio support Typescript? It doesn't seem so.
Here is the code of custom component:
https://jsfiddle.net/kv0tq9p8/
Hey I am a newcomer in this project and I would like to contribute
Hi,
I have a React18 project and wanted to make custom react-formio component (code below) but
this.container.unmount
indetachReact
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.
@FractalStranger did you figure any way to get around this?
@FractalStranger
Hello, did you find someway to figure this out yet? Same issue I also encoutered.
Hey @YoungDriverOfTech @lotorvik @FractalStranger thanks for chiming in here. A couple of things to consider:
- 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.
- 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. - 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;
}
}