Skip to content

Latest commit

 

History

History
234 lines (165 loc) · 10.2 KB

using-third-party-react-components.md

File metadata and controls

234 lines (165 loc) · 10.2 KB

Using third party React components

Using a third party (Javascript) React component is straightforward for most components. There are three ways of declaring a third party React component in F# - either by declaring a Discriminated Union where each case has one field; by declaring a record type for the props with the Pojo attribute; or by using an untyped list of (string * obj) tuples. All three ways are described below.

Some components have a Typescript definition available, either because the component was authored in Typescript or someone created a type definition for the Definitely Typed project. If this is the case then you can try the ts2fable tool to convert this React component type definition from Typescript to a Fable type declaration - it might need some tweaking but for components with a big API surface this can be a real time saver.

Table of contents

Using a React component by declaring a Discriminated Union props type

The basic steps when working with a Discriminated Union are:

1. Install the react component

Using yarn or npm, install the react component you want to use.

For example to use the rc-progress React component which we'll be using in this tutorial, run the following command inside your Fable project root folder:

yarn add rc-progress

2. Define the props type

Reference the documentation of the React component to find out which props the component supports and declare them as an F# type (see below for the two supported mechanisms). You can define only a subset of supported props in F# if you don't need to cover the full props options that the React component supports.

For example to expose the percent, strokeWidth and strokeColor props of the rc-progress components:

type ProgressProps =
  | Percent of int
  | StrokeWidth of int
  | StrokeColor of string

If one of the props is treated as a string enum in Javascript (e.g. if there is a size prop with the supported values "small", "normal" and "big"), then the [<StringEnum>] attribute can be very useful for defining helper types (see the StringEnum docs for more info):

[<StringEnum>]
type Size =
  | Small
  | Normal
  | Big

type SomeComponentProps =
  | Size of Size
  | ...

3. Define the React component creation function

There are several different ways to declare exports in Javascript (default imports, member imports, namespace imports); depending on how the Javascript React component was declared, you have to choose the right import. Refer to the Fable docs for more information on imports.

Using the ofImport function you instruct Fable which component should be instantiated when the creation function is called.

Member Import

In the example of rc-progress, to declare a progressLine creation function that imports the Line component from the library rc-progress, you would declare it as follows.

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

let inline progressLine (props : ProgressProps list) (elems : ReactElement list) : ReactElement =
    ofImport "Line" "rc-progress" (keyValueList CaseRules.LowerFirst props) elems

The keyValueList function is used to convert the props of type IProgressProps list to a JavaScript object where the key is the lower case name of the discriminated union case identifier and the value is the field value of the discriminated union (e.g. if the list that is passed into the function is [Percent 40; StrokeColor "red"], the Javascript object that will be passed to the props of the Line react component would look like this: { percent: 40, strokeColor: "red" })

In the docs of the rc-progress React component the import style used is a member import (e.g. import { Line, Circle } from 'rc-progress';), so we refer to the component member Line directly in the ofImport expression.

Default Import

If the export is declard as a default export, then you would use "default" as the member name. Taking react-native-qrcode-scanner as an example:

To translate the example

import QRCodeScanner from 'react-native-qrcode-scanner';

you would declare your function like

let inline qr_code_scanner (props : QRCodeScannerProps list) : ReactElement =
    ofImport "default" "react-native-qrcode-scanner" (keyValueList CaseRules.LowerFirst props) []

Fields of imported items

Some React components must be instantiated as follows in JS:

import { Select } from 'react-select'
let render = () => <Select.Creatable options={...} />

In this case, you can also use ofImport to directly access the field of the imported item:

// Import { Select } from "react-select" and then access the "Creatable" field
ofImport "Select.Creatable" "react-select" myOptions []

// Also compatible with default imports
ofImport "default.Creatable" "react-select" myOptions []

Directly creating the element

If you already have a reference to the imported component, then you can also use createElement.

The default import above could also be rewritten like this:

let rnqs = importDefault "react-native-qrcode-scanner"
createElement(rnqs, (keyValueList CaseRules.LowerFirst props), [])

Please note it's also OK to duplicate ofImport with same member and path. In this case, Fable will automatically group the imports.

let foo1 = ofImport "default" "react-foo" { height = 25 } []
let foo2 = ofImport "default" "react-foo" { height = 50 } []

4. Use the creation function in your view code

The function you declared in step 2 now behaves just like any other React component function.

To use the component in a Fable-Elmish view function:

let view (model : Model) (dispatch : Msg -> unit) =
  div
    []
    [ progressLine [ Percent model.currentProgress; StrokeColor "red" ] [] ]

5. Get component state back into your code

If you want to get from your component state back in F# code, you need to follow react documentation : https://reactjs.org/docs/lifting-state-up.html to propagate component state to upper components.

Insert a function in your props to get state back.

// Define function in props
type ComponentProps =
  | GetState of (DateTime -> unit)
  | ...

// sample function matching props signature
let logDateTimeSelected (d : DateTime) =
  printfn "Date : %A" d

// Component definition (here a calendar)
let inline Calendar (props : ComponentProps list) : Fable.React.ReactElement =
    ofImport "default" "./CalendarComponent.tsx" (keyValueList CaseRules.LowerFirst props) []

// Component initialization with props
div [] [
  Calendar [ GetState logDateTimeSelected ]
]

On Javascript/TypeScript side, you need to use this props within your component

// Define component props
export interface IComponentProps {
  // new props matching F# ComponentProps definition
  getState : ((date : Date) => void)
  ...
}

// within JS/TS event handler use your props function
  private _onSelectDate(date: Date): void {
    this.props.getState(date)
    ...
  }

Importing using a record

This is similar to the approach above, but instead of declaring a DU you create a record. Using a record to express the props looks more like idiomatic F# code but it can be unwieldy if you have a lot of optional props. Since this is common with React components, using the DU approach above can often be more convenient.

type ProgressProps =
  { percent : int
    strokeWidth : int
    strokeColor : string
  }

let inline progressLine (props : ProgressProps) (elems : ReactElement list) : ReactElement =
    ofImport "Line" "rc-progress" props elems

Untyped props

The third way of using a React component is to not give an F# type to the Props at all and simply pass a list of (string * obj) tuples to the createObj helper function which turns the list into a Javascript object and passes it as the props of the React component. This of course has the least level of type safety but it can be convenient for prototyping. The ==> operator is defined in the Fable.Core.JsInterop module to make (string * obj) tuple creation easier to read.

open Fable.Core.JsInterop

ofImport "Line" "rc-progress" (createObj ["strokeWidth" ==> 5]) []

// You can also use anonymous records
ofImport "Line" "rc-progress" {| strokeWidth = 5 |} []

Edge cases

This documentation needs to be extended to cover Higher Order Components and maybe Context, Fragments etc. Contributions are welcome!