react icon indicating copy to clipboard operation
react copied to clipboard

[BUG] migrating from react-formio - what replaces "ref" prop?

Open adventmedia opened this issue 3 years ago • 8 comments

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?

adventmedia avatar Aug 18 '21 19:08 adventmedia

I also can reproduce the issue reported by @adventmedia . I got the following warning message in the console log.

image

Could you mind telling me if there's a way to get the reference? Thank you very much.

yuenyuen2010 avatar Aug 19 '21 09:08 yuenyuen2010

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

j-perl avatar Oct 25 '21 22:10 j-perl

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.

devcovato avatar Feb 03 '22 15:02 devcovato

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 avatar Apr 17 '23 13:04 cesosag

@cesosag That's right. Nowadays, I use the formReady combined with React.useRef(), and programmatically I run validation checkValidity(...) and form submission submit().

devcovato avatar Apr 18 '23 18:04 devcovato

@cesosag That's right. Nowadays, I use the formReady combined with React.useRef(), and programmatically I run validation checkValidity(...) and form submission submit().

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: image image

namti avatar Aug 22 '23 04:08 namti

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

namti avatar Aug 22 '23 21:08 namti

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.

devcovato avatar Aug 31 '23 08:08 devcovato