react
react copied to clipboard
[BUG] migrating from react-formio - what replaces "ref" prop?
Environment
- Hosting type
- [ ] Form.io
- [x] Local deployment
- Version: 5.1.1
- Formio.js version: 4.13.2
- Frontend framework: react 16.13.1
Migrating to @formio/react
from react-formio
:
We were using the ref
prop to return the instance of the formio object to access its apis:
<Form
ref={(form: any) => setFormObj(form)}
onSubmit={() => {
ClientLogger.error('FormDisplayPrevious.onsubmit', 'Called submit');
}}
options={{ noAlerts: true, readOnly: true, getData: (id: string) => getForm(id), inEditor: false }}
form={parsedForm}
onPrevPage={resetScroll}
onNextPage={resetScroll}
onCustomEvent={formCustomEvent}
onRender={formReady}
/>
ref
prop has been removed - how do we now get access to form
?
I also can reproduce the issue reported by @adventmedia . I got the following warning message in the console log.
Could you mind telling me if there's a way to get the reference? Thank you very much.
Until the Form component gets wrapped with forwardRef, this is currently not possible. The way I found to get around this was to use our own component for the time being. Also, the reason why ref worked before was that the Form component was a class component. It has since then been converted to a functional component and to keep having access to the internal ref, we need to forward the ref with forwardRef(). Something like this:
const ReactFormio = forwardRef<any, Props>((props, ref) => {
...
const attachForm = (divRef: any) => {
if (divRef && !instance) {
if (!options.events) {
options.events = getDefaultEmitter()
}
const formOrSrc = form || src
const formioInstance = new (formioform || Form)(divRef, formOrSrc, options)
setInstance(formioInstance)
setRef(ref, formioInstance)
}
}
return <div ref={attachForm} />
Hi,
Like the reporter, I need to access the formio object. Locally, I modified the file https://github.com/formio/react/blob/master/src/components/Form.jsx as follows:
import React, { useCallback, useEffect, useImperativeHandle, useReducer, useRef } from "react"
import PropTypes from "prop-types"
import EventEmitter from "eventemitter2"
import _isEqual from "lodash/isEqual"
import { Formio } from "formiojs"
const FormioForm = Formio.Form
const formioReducer = (state, action) => {
switch (action.type) {
case "SET_FORMIO":
return action.payload
case "SET_FORMIO_FORM":
if (state) {
state.form = action.payload
}
return state
case "SET_FORMIO_SRC":
if (state) {
state.src = action.payload
}
return state
case "SET_FORMIO_SUBMISSION":
if (state) {
state.submission = action.payload
}
return state
case "SET_FORMIO_URL":
if (state) {
state.url = action.payload
}
return state
default:
return state
}
}
const Form = (props, ref) => {
const instance = useRef(undefined)
const createPromise = useRef(undefined)
const elementRef = useRef(null)
const [formio, dispatch] = useReducer(formioReducer, undefined)
const { src, form, url, options = {}, formioform, formReady, submission } = props
useImperativeHandle(
ref,
() => {
return formio
},
[formio]
)
useEffect(() => () => formio ? formio.destroy(true) : null, [formio])
const createWebformInstance = useCallback(
srcOrForm => {
instance.current = new (formioform || FormioForm)(elementRef.current, srcOrForm, options)
createPromise.current = instance.current.ready.then(formioInstance => {
dispatch({ type: "SET_FORMIO", payload: formioInstance })
if (formReady) {
formReady(formioInstance)
}
})
return createPromise.current
},
[options, formioform, formReady]
)
const initializeFormio = useCallback(() => {
const onAnyEvent = (event, ...args) => {
if (event.startsWith("formio.")) {
const funcName = `on${event.charAt(7).toUpperCase()}${event.slice(8)}`
// eslint-disable-next-line no-prototype-builtins
if (props.hasOwnProperty(funcName) && typeof props[funcName] === "function") {
props[funcName](...args)
}
}
}
if (createPromise.current) {
instance.current.onAny(onAnyEvent)
createPromise.current.then(() => {
if (submission) {
dispatch({ type: "SET_FORMIO_SUBMISSION", payload: submission })
}
})
}
}, [submission, props])
useEffect(() => {
if (src) {
createWebformInstance(src).then(() => {
dispatch({ type: "SET_FORMIO_SRC", payload: src })
})
initializeFormio()
}
}, [src, createWebformInstance, initializeFormio])
useEffect(() => {
if (form) {
createWebformInstance(form).then(() => {
dispatch({ type: "SET_FORMIO_FORM", payload: form })
if (url) {
dispatch({ type: "SET_FORMIO_URL", payload: url })
}
})
initializeFormio()
}
}, [form, url, createWebformInstance, initializeFormio])
useEffect(() => {
if (!options.events) {
options.events = Form.getDefaultEmitter()
}
}, [options.events])
useEffect(() => {
if (formio && submission && !_isEqual(formio.submission.data, submission.data)) {
dispatch({ type: "SET_FORMIO_SUBMISSION", payload: submission })
}
}, [submission, formio])
return <div ref={elementRef} />
}
Form.getDefaultEmitter = () => {
return new EventEmitter({
wildcard: false,
maxListeners: 0
})
}
const ReactFormio = React.forwardRef(Form)
ReactFormio.propTypes = {
src: PropTypes.string,
url: PropTypes.string,
form: PropTypes.object,
submission: PropTypes.object,
options: PropTypes.shape({
readOnly: PropTypes.bool,
noAlerts: PropTypes.bool,
i18n: PropTypes.object,
template: PropTypes.string,
saveDraft: PropTypes.bool
}),
onPrevPage: PropTypes.func,
onNextPage: PropTypes.func,
onCancel: PropTypes.func,
onChange: PropTypes.func,
onCustomEvent: PropTypes.func,
onComponentChange: PropTypes.func,
onSubmit: PropTypes.func,
onSubmitDone: PropTypes.func,
onFormLoad: PropTypes.func,
onError: PropTypes.func,
onRender: PropTypes.func,
onAttach: PropTypes.func,
onBuild: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onInitialized: PropTypes.func,
formReady: PropTypes.func,
formioform: PropTypes.any
}
export default ReactFormio
The compilation process provides the green light. 🙂
Below is the usage where the form schema doesn't contain a submit button:
NOTE: the provided code is not the same one I use but should give an idea
import ReactFormio from "./ReactFormio"
const MyForm = ({ schema, submission, onSubmit }) => {
const formioRef = useRef(null)
const formOptions = {noAlerts: true}
const onNextClick = () => {
if (formioRef.current) {
formioRef.current.submit()
}
}
return (
<>
<ReactFormio ref={formioRef} form={schema} submission={submission} onSubmit={onSubmit} options={formOptions} />
<Button onClick={onNextClick}>Next</Button>
</>
)
}
The browser console reports:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in ForwardRef(Form) (at MyForm.jsx:15)
I don't have much experience in React, and I don't know where to look at it. Running the application so far didn't show any issues.
I suppose you need access to the ref in order to have access to the form
instance of formio
. Instead of using a ref
you can use the formReady
prop callback to get the form instance back.
@cesosag That's right. Nowadays, I use the formReady
combined with React.useRef()
, and programmatically I run validation checkValidity(...)
and form submission submit()
.
@cesosag That's right. Nowadays, I use the
formReady
combined withReact.useRef()
, and programmatically I run validationcheckValidity(...)
and form submissionsubmit()
.
Hi, @devcovato I have tried using formReady
with React.createRef
but the formio
is null
. Could you please give us an example how you achieved this? Thanks a lot
This is what I get from the formReady
callback:
I figured it out.
constructor(){
this.formRef = React.createRef();
}
checkValidity(){
this.formRef.checkValidity(this.formRef.submission, true);
}
<button onClick={this.checkValidity}>Validate</button>
<Form {...otherProps} formReady={this.formRef} />
Hi @namti, I'm sorry for the slow reply.
I changed the application code. I use a custom Form
similar to the code above.
Maybe it's not React-way. I create const formRef = React.useRef(null)
and pass it around. In the wrapper component containing the Form component, I set the formReady
handler to assign the form
to formRef
. In the button click handler, I fetch formio
from formRef.current
, check the validation and programmatically submit the form. Something like:
const formio = formRef.current
if (formio.checkValidity(null, true, null, false)) {
formio.nosubmit = true
formio.submit()
}
I hope it will help you.