Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Reference to JSX Component generates inline function, a lost state and performance problem

Open playfulThinking opened this issue 2 years ago • 1 comments

Description

When I pass a JSX.Component to React Navigation (https://reactnavigation.org) it gets compiled into an inline function call, which is inefficient in React (and React Native, which I'm using).

Repro code

in "WinScreen.fs"

[<JSX.Component>]
let TestScreen () =
    JSX.jsx $"""
    <View>
        <Text> Hi There </Text> 
    </View>
    """

in "App.fs"

[<JSX.Component>]
let Render2 () =
    JSX.jsx $"""
    <NavigationContainer>
            <Stack.Screen name="win" component={WinScreen.WinScreen} />
            </Stack.Navigator>
    </NavigationContainer>
    """ |> toReact

Expected and actual results

I expected the {WinScreen.WinScreen} reference to generate this:

<Stack.Screen name="win" component={WinScreen}  />

But it generates this inline function:

<Stack.Screen name="test" component={() => <WinScreen></WinScreen>} />

and I get this warning in Chrome DevTools console:

Looks like you're passing an inline function for 'component' prop for the screen 'test' (e.g. component={() => <SomeComponent />}). Passing an inline function will cause the component state to be lost on re-render and cause perf issues since it's re-created every render. You can pass the function as children to 'Screen' instead to achieve the desired behaviour.

I can solve this problem by writing:

emitJsStatement () "import {WinScreen} from '../src/WinScreen.fs.js'"

...
<Stack.Screen name="win" component={{WinScreen}}  />

which generates what I hoped to get:

<Stack.Screen name="win" component={WinScreen} />

I'm fine with doing this, but it's hacky.

Am I doing something wrong or is this an inadvertent un-optimization? Thanks much!

Related information

  • Fable version: 4.5.0
  • Operating system: Mac Ventura 13.6.2

playfulThinking avatar Dec 20 '23 20:12 playfulThinking

The AST generated from the FSharp you're passing through is either an empty constructor or a function. Therefor, this is the correct behaviour.

Perhaps some sugar could be provided as an operator that converts an a 'T -> JSX.Element into a container value which is then rendered as an IdentExpr.

I've implemented this in Partas.Solid as !@.

Outside of something like that being natively supported, you can extrapolate the logic from my plugin and make your own which performs the transformation on the calling of the operator. Just make sure you don't dispose of the import when transforming to an IdentExpr

Otherwise you can do the second option; it's not really hacky, you're just escaping string interpolation. But if you don't reference that type/function anywhere else in the file then nothing will tell Fable to import it

shayanhabibi avatar Mar 15 '25 09:03 shayanhabibi