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

Component created with a forwardRef that has generic type parameters loses state #597

Closed
Jacques2Marais opened this issue Mar 4, 2024 · 2 comments

Comments

@Jacques2Marais
Copy link

Jacques2Marais commented Mar 4, 2024

I have two components, GenericInputWithForwardRef and GenericInputWithoutForwardRef. Both these components have generic type parameters in the F# function definition. The first component also uses React.forwardRef in its definition. Here is the setup:

let GenericInputWithForwardRef<'t> = React.forwardRef (
   fun ((), ref) ->
      let value, setValue = React.useState ""
      Html.input [
         prop.value value
         prop.onChange setValue
         prop.ref ref
      ]
)

let GenericInputWithoutForwardRef<'t> () =
   let value, setValue = React.useState ""
   Html.input [
      prop.value value
      prop.onChange setValue
   ]

I then use these components as following

let refO = React.useRef None

React.fragment [
  GenericInputWithForwardRef ((), refO)
  GenericInputWithoutForwardRef ()
]

The problem is, whenever I change the value of the second component, the first component unmounts and mounts again, thus losing its state and value. After some debugging, I also realized that I forgot to add the [<ReactComponent>] attribute above the two component definitions. But doing this causes another issue:

image

Looking at the generated code on line 652 of FormTesting.fs.js, I see the following:

const xs = [createElement(GenericInputWithForwardRef, {})([void 0, refO]), createElement(GenericInputWithoutForwardRef, null)];
return react.createElement(react.Fragment, {}, ...xs);

And the error seems to be right after the part createElement(GenericInputWithForwardRef, {}) (the first create element). The generated code for GenericInputWithForwardRef is as following

export function GenericInputWithForwardRef() {
    return React_forwardRef_3790D881((tupledArg) => {
        const ref = tupledArg[1];
        const patternInput = useFeliz_React__React_useState_Static_1505("");
        const value = patternInput[0];
        const setValue = patternInput[1];
        return createElement("input", {
            value: value,
            onChange: (ev) => {
                setValue(ev.target.value);
            },
            ref: ref,
        });
    });
}

The issue seems to be perhaps with Feliz' implementation of React.forwardRef?
Thank you.

@lukaszkrzywizna
Copy link
Contributor

lukaszkrzywizna commented Mar 26, 2024

Firstly, the ReactComponent attribute isn't necessary for React.forwardRef; this function inherently creates a component.

The issue lies in how Fable handles generics—it transforms them into functions, whereas typically, they might be fields. Consider the following examples:

let NonGeneric = React.forwardRef(fun (props: {| x: int |}, ref) -> Html.span "Hello Non Generic!")

let Generic<'t> = React.forwardRef(fun (props: {| x: 't |}, ref) -> Html.span "Hello Generic!")
// Const - OK, component is defined once
export const NonGeneric = React_forwardRef_3790D881((tupledArg) => {
    const props = tupledArg[0];
    const ref = tupledArg[1];
    return createElement("span", {
        children: ["Hello Non Generic!"],
    });
});

// Function - not OK, component is re-defined with every render
export function Generic() {
    return React_forwardRef_3790D881((tupledArg) => {
        const props = tupledArg[0];
        const ref = tupledArg[1];
        return createElement("span", {
            children: ["Hello Generic!"],
        });
    });
}

There are three solutions:

  1. Define a function that includes a predefined generic parameter:
let StringSpecific = Generic<string>
// It's again a const defined once during module loading
export const StringSpecific = Generic();
  1. Rely on useCallback for optimization. This approach is somewhat intricate: you must create an additional component that yields your generic component. By wrapping this intermediary component with useCallback, it ensures that it is instantiated only once.
[<ReactComponent>]
let Generic<'t> x = x |> React.useCallback(React.forwardRef(fun (props: {| x: 't |}, ref) ->
  Html.span "Hello Non Generic!"))
export function Generic(genericInputProps) {
    const x__1 = genericInputProps.x_1;
    const x_ = genericInputProps.x_0;
    const x = [x_, x__1];
    return useReact_useCallback_1CA17B65(React_forwardRef_3790D881((tupledArg) => {
        const props = tupledArg[0];
        const ref = tupledArg[1];
        return createElement("span", {
            children: ["Hello Non Generic!"],
        });
    }))(x);
}
  1. [Prefered] Rename the ref prop to a different identifier. Given that React plans to eliminate the necessity for forwardRef (details available here), you can adapt by simply using a different name than ref. The React mechanism only searches for the prop name; no additional effort is required.

I hope this clarifies the core of the issue and allows us to close it. 😄

@Jacques2Marais
Copy link
Author

Thank you @lukaszkrzywizna. I have indeed switched to using the ref prop under a different identifier. I will now close the issue.

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

No branches or pull requests

2 participants