Feliz: [<ReactComponent>] vs React.functionComponent in React refresh

Created on 25 Nov 2020  ·  10Comments  ·  Source: Zaid-Ajaj/Feliz

Hello friend,

as naive wannabe frontend dev I came into interesting behavior:

Based on your docs, I favour using React.functionComponent to create components because I can also specify the name of component for better debuging. Funny thing comes with react refresh.

Code like this always trigger full page reload when doing any change in code:

let subView = React.functionComponent("SubView", fun () ->
    Html.text "Hello from sub"
)

let topView = React.functionComponent("TopView", fun () ->
    Html.div [ Html.div "Hello from top"; subView () ]
)

image

But having root component with [<ReactComponent>] will make all refresh working great without doing full refresh.

let subView = React.functionComponent("SubView", fun () ->
    Html.text "Hello from sub"
)

[<ReactComponent>]
let topView () = Html.div [ Html.div "Hello from top"; subView () ]

Is it bug? Is it bug in fancy black tie aka feature? Is it all my fault because I don't get the difference between ReactComponent attribute and React.functionComponent function? 😄

Thanks for any kind of help to better understand.

question

Most helpful comment

Wow! Just... WOW! Silly person asks stupid question and smart guy answers with three A4 sheets full of relevant information and explanations. 🤯 You just rock, man! Thanks a lot! ❤️ Now I understand and will start using Attribute instead.

Is it still recommended to name components somehow (like we did using the first parameter of React.functionComponent) or this it's not necessary anymore for attribute approach?

All 10 comments

The attribute will be the way to go in the future, and is necessary for react-refresh to work. At least, that's how it was but I thought @Zaid-Ajaj got that sorted out so that there was no difference?

Hi Roman,

This is a really good question to which there is no short answer, here we go.

Though once I sort out my internet issues at home, I will resume streaming again where I explain this in excruciating details 😁

Based on your docs, I favour using React.functionComponent to create components because I can also specify the name of component for better debugging. Funny thing comes with react refresh.

First of all, I want to point out that the documentation are in an unfortunate transitional state: basically out of date with latest Feliz and [<ReactComponent>] goodness until we sort out the compiler plugin issues and the stabilize the AST used by Fable. What I am trying to say is that the docs do not favor React.functionComponent and if you are using Fable 3, you should start using [<ReactComponent>] instead.

This has to do with how the Javascript code is generated. Now consider this piece of React code:

const App = ({ title }) => {
  return (
    <h1>{title}</h1>
  )
};

<App title="My React Application" />

This JS (which is actually JSX) desugars down to the following JS code

import { createElement } from 'react'

const App = ({ title }) => {
  return createElement("h1", null, title);
};

// <App /> compiles to a call to createElement
createElement(App, { title: "My React Application" })

It is very important to notice that the initialization of the App component via <App ... /> is compiled to a call to createElement from JS.

It is important because of two things:

  • It separates the definition of the component from its creation
  • It allows App to be a static value (identity is reserved which React makes use of)

Now, the way React.functionComponent is implemented is "bad" for React because it combines the definition and creation in one go. This F# code

let app React.functionComponent("App", fun (props: {|  tite: string |}) -> Html.h1 props.title)

Is kind-of equivalent to saying

let app = 
  let render(props: {|  tite: string |}) = 
    let renderFn props= Html.h1 props.title
    createElement(renderFn , props)
  render.displayName <- "App"
  render

Now this "bad" because createElement renders a component that is defined dynamically on each invocation and it mutates the displayName of the function rather than using the function name itself.

This syntax sugar the React.functionComponent employs doesn't affect the runtime behaviour of React and it is why we are able to use it without problems to build React application. The problems start to creep out when the TOOLING around React applications starts to analyze the generated code: webpack, babel, react-refresh are all tools that look the generated code, inject some magic here and there to make hot module replacement possible but with React.functionComponent it is not possible because the generated code doesn't conform to how React applications are usually built when you are writing JS and many of the tooling around React make assumptions based on the fact the the code is written according to standard React that than generated by a tool.

The solution to these problems is coming in Fable 3 where we are allowed to customize the compilation of the code via compiler plugins. If you consider this F# Feliz code in Fable 3

[<ReactComponent>]
let app (title: string) = Html.h1 title 

app "My  React Application"

Then it compiles down to the equivalent of this JS (not exact because more magic)

const App = (props) { 
   const title = props.title
   return createElement("h1", null, title)
}

createElement(App, { title: "My  React Application" })

Now this generated code is customized to look like what React tooling expects:

  • Definitions and creations of function components are separate
  • Components are defined as upper case (yes, this is also required and the compiler plugin automatically rewrites the definition)
  • Input parameters are automatically translated into React props

This combination makes React refresh work and by extension makes the experience of Feliz really close to that of native JS or TS. I hope this clears up the confusion. If you have any more questions, just let me know 😉

Wow! Just... WOW! Silly person asks stupid question and smart guy answers with three A4 sheets full of relevant information and explanations. 🤯 You just rock, man! Thanks a lot! ❤️ Now I understand and will start using Attribute instead.

Is it still recommended to name components somehow (like we did using the first parameter of React.functionComponent) or this it's not necessary anymore for attribute approach?

Now I understand and will start using Attribute instead.

Awesome! Let me if you encounter any problem 😉 right now the only issue to be aware of is using a record type as input props will be just a little bit problematic for React-refresh, so you can use an anonymous record instead or primitive parameters. I am hoping the issue is fixed soon so that I can document [<ReactComponent>] without having any caveats.

Is it still recommended to name components somehow (like we did using the first parameter of React.functionComponent) or this it's not necessary anymore for attribute approach?

No, the only requirement is that the components are compiled as upper-case values which is one of the things that [<ReactComponent>] does to the function definition :smile: you can the implementation here

@Dzoukr the only reason to use names for your react components is to make debug tooling better.

@Shmew Yup, but there is no overload on ReactComponent constructor for adding such name so that's why asked.

So, continuing the [<ReactComponent>] vs functionComponent questions: hooks!

I've got a component that needs to initialize some state. Fortunately, we have many fine hooks available! I write this:

[<ReactComponent>]
let Entrypoint() =
    let drawing, updateDrawing = React.useState(Deferred.HasNotStartedYet)
    let loader = React.useDeferredCallback((fun () -> loadDrawing()), updateDrawing)

    React.useEffect(loader, [||])
    // etc

Alas, when I try to run this, my console is sad and full of errors:

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
    React 

I've been through the react debug steps above; none appear to be true. Should this be working? Is this a Fable 3 only thing? (I'm still using Fable 2.)

My understanding is that the reactcomponent attribute needs fable 3 to work because it is a fable 3 compiler plugin

@landy ah, that'd sure do it. Lemme get Fable 3 working, will report back in a minute

Hey! Yep, that fixed me -- I used this PR as a guide for the conversion.

Was this page helpful?
0 / 5 - 0 ratings