fable-compiler / fable-react

Fable bindings and helpers for React and React Native
MIT License
275 stars 66 forks source link

Can't figure out how to wrap my view with a Higher Order Component #212

Closed enriquein closed 3 years ago

enriquein commented 3 years ago

I'm currently trying to add translation support to our Fable app with react-i18next (https://react.i18next.com). My first attempt was to wrap my view functions as functional components (by calling ofFunction) so that I could get access to the translation API through a react hook. The problem with this approach is that any textboxes inside my component get re-rendered on each keystroke. I believe this is caused by react re-rendering the entire functional component on each event.

There is another option that provides access to the translation API by using a Higher Order Component. I can't figure out how to write F# that can be translated to their example:

import React from 'react';
import { withTranslation } from 'react-i18next';

function MyComponent({ t, i18n }) {
  return <p>{t('my translated text')}</p>
}

export default withTranslation()(MyComponent);

I tried googling and looking at current and older github issues but couldn't find much info about consuming 3rd party HOCs, nor people who had stumbled into 3rd party APIs that exported values using that syntax (func()(args)).

I guess I have 2 questions:

My end goal would be to open a documentation PR to document these topics for future users trying to learn Fable.

Thanks!

alfonsogarciacaro commented 3 years ago

Yes, we were discussing about adding helpers for HOC but it seems they're not so common in React now so we didn't add it at the end. This one is also challenging for several reasons, as it being a function returning a function as you mention. This is actually a curried function but the problem is Fable tries to uncurry functions so using a signature like unit -> ReactElementType -> ReactElementType won't work. There are workarounds for this, but probably here the simple one is using Emit. I think the following code should work:

open Fable.Core
open Fable.Core.JsInterop
open Fable.React

[<Emit("$0.t($1)")>]
let t (props: 'Props) (text: string): string = jsNative

[<ImportMember("react-i18next"); Emit("$0()($1)")>]
let withTranslation(render: 'Props -> ReactElement): ReactElementType<'Props> = jsNative

// Usage
let private myComponent (props: {|foo: int|}) =
    p [] [str (t props "my translated text")]

let MyComponent = withTranslation myComponent

let render() =
    ReactElementType.create MyComponent {| foo = 5|} []

Another challenge is the props of your component shouldn't include t or i18n (if they do, the F# compiler will ask you to fill out those values when calling the component), so I've added an extra helper that also uses emit to access the t field from your props.

enriquein commented 3 years ago

Thank you! This worked as far as getting the HOC to work as expected. This will definitely help us going forward as we continue to consume more 3rd party components and libraries.

However, I feel like I need to mention for the benefit of future people coming to this issue, that using react-i18next as a HOC did not fix the problem of re-rendering of controls on each event until we used the new FunctionComponent feature (https://fable.io/blog/Announcing-Fable-React-5.html).

Now it all works. Thank you so much for the quick reply!

Once we figure this out I will be creating a pull request to document this.

alfonsogarciacaro commented 3 years ago

Just to let you know, the attention of the community is shifting towards Feliz and the new ReactComponent attribute instead of FunctionComponent. But I think it's not possible to use it with HOC yet. I've opened an issue for that: https://github.com/Zaid-Ajaj/Feliz/issues/304

enriquein commented 3 years ago

After playing around with FunctionComponent we were pleasantly surprised to see that our old Elmish view that used the hook did not re-render after each input when wrapped as a function component. Since this is much simpler than using the HOC, and already had interfaces for the hook API, we decided to use FunctionComponent with the hook instead.

Again I would like to say how much we appreciate your help and guidance through this process.

We will definitely look into Feliz if this is the general direction the community is going in.