It is possible to directly name a react component and avoid the `make` convention?

Hello,

I’m giving a try to Rescript (and by the was I was really confused between ReasonML & Rescript) and so I’m using it in a create-react-app project.
I have some habits from my JS background and especially I really don’t like the default export. So as soon as I create a React Component I choose to export it explicitly
And now I was trying to rewrite the default App.js to a default App.res file but I can’t really get my way out of this.
It seems that if I don’t write it with the make convention (which is kind of weird from my perspective) it doesn’t even compile.

FAILED: src/App.ast
Fatal error: exception Invalid_argument(“react.component calls cannot be destructured.”)

And If I use the make convention, I have to deal with the really weird and disturbing (again from my perspective) convention to import it this way:

import { make as App } from “./App.bs.js”

Where I want to write it like this:

import { App } from “./App.bs.js”

So… question would be: is it possible simply?
And what happen if I have several react components from the same file?

First of all, it’s not possible to export a function with name App, because in rescript function names should start from a lowercase letter.

If you really have to rename the make function, you can add another binding after the component definition.

let app = make

And in JS you can import it as:

import { app as App } from “./App.bs.js”;
1 Like

With genType it is possible to do something like this:

// File: MyComponent.res
@genType.as("MyComponent")
let myComponent = make

and then import like this
import { MyComponent } from 'path/to/MyComponent.gen.js'

Maybe you could even put the genType.as directly above make function. I haven’t tried.

Edit: ok, at least it compiles on playground: https://rescript-lang.org/try?code=PTAEDEEsBsFNQEIFcAuKD2A7AdAJ1gM4BQAAgOayYAqAngA6zYCGBAFAETJpbsCUoJfEwDGKbMPQBbOlkooicFKElMA1vAC8oVvw0A+UAG8ioUAB4ARqgyYDh9gDN06dgFo9AJVgixBFLkhMMgBfc2ArblsiYKIgA

4 Likes

Thanks vdanchenkov and TomiS for your answers.
So, as far as I understand, it is not possible to declare several react components in the same file because of the constraint of the make variable?
I have read that there is still work in progress related to the react-rescript library, is it something likely to change? Or - to the contrary - are the choices and solutions provided here quite final?

It highly depends on your use-case, so first of all, what is your goal?

Do you want to create ReScript React components that…
A) are being used exclusively on JS side
B) are being used in ReScript AND JS

The make convention is only needed if you want to use your component in ReScript JSX (B).

If you are only creating some React components that are being used on JS side, you could also use a different name than make:

@react.component
let button = () => {
  <div />
}

@react.component
let avatar = (~src) => {
  <img src/>
}

But since in ReScript it’s not allowed to use capitalized names for bindings, you’d still need to rename it on JS import side.

The last resort, which I would not recommend, but what @react.component internally does, is use the escape syntax to still have uppercased binding names:

@react.component
let \"Button" = () => {
  <div />
}

@react.component
let \"Avatar" = (~src) => {
  <img src/>
}

Now, if you completely drop the @react.component, you essentially get the pure JS equivalent of defining a component.

let \"Avatar" = (props: {"src": string}) => {
  let src = props["src"]
  <img src/>
}

Please note that the last few examples would not allow you to use the component in any ReScript JSX, since it doesn’t follow the make binding convention.

4 Likes

To follow up on case B) where you want to define multiple components in one component for ReScript JSX usage, you need to use submodules:

// Navigation.res
module Button = {
  @react.component
  let make = (~children) => {
    <button> children </button>
  }
}

module Avatar = {
  @react.component
  let make = (~src) => {
    <img src />
  }
}

// This is the actual `<Navigation />` make function
@react.component
let make = () => {
  <div>
    <Avatar src="patrick.jpg" />
    <Button> {React.string("hi")} </Button>
  </div>
}

Looking at the JS output, it’s probably a little harder to import those components conveniently with JS import though, since they essentially export as objects, such as { Button: { make: ...} }, so on the JS import side, you’d need to access the nested make function instead.

3 Likes

Thanks Ryyppy for your answers.
My goal is to find an alternative solution to Typescript and JS. I want the typing with as much inference as I can get.
So I’m trying ReScript to see if it can fit to my needs.
I want to be able to use JSX because it’s cleaner in my opinion. I like when the syntax is light and don’t get in the way.
I’m trying to get it working with both JS and ReScript for now, but ultimately, if ReScript is my cup of tea, I don’t see why I should use JS anymore.

In my opinion, for now, with the little of what I know of ReScript:
I like constraints if these forbids you bad habits. However, the constraints around make and uppercase convention feels more like being incompatible with React and so we have to hack around with a renamed import to make it work.
Furthermore, with hooks, I often have to declare another component within the same file just because the latter use a hook and it’s for example declared within a map. And it doesn’t have any sense (for me) to put it on another file because it’s just too short, too simple, and it’s better understood when kept with its parent. So I don’t require to export it then to another file so your last solution looks pretty good to me.

In fact, it looks so good that I want to do that for every component, so I don’t have to worry about the make convention or the uppercase convention. But when I try to export a module, it is exported as:

var App = {
make: App$App
};

So I would have to then make a reference like App.make in my JSX to make it work? Why not…

1 Like

So I tried the App.make shape for usage within JSX but obviously I forgot that there is a rule against this.
Line 9:5: Imported JSX component make must be in PascalCase or SCREAMING_SNAKE_CASE react/jsx-pascal-case

So I could deactivate this rule but I would prefer not.
So I see only two choices from my perspective:

  • deactivate a rule which try to set a standard of practices
  • re-define name of exported function every time

I also tried genType as suggested by TomiS but it does not export as intended the new type… for example here is what I get in my App.bs.js file:

  return React.createElement("div", {
              className: "App"
            }, React.createElement("header", {
                  className: "App-header"
                }, React.createElement("img", {
                      className: "App-logo",
                      alt: "logo",
                      src: logo
                    }), React.createElement("p", undefined, "Edit <code>src/App.res</code> and save to reload."), React.createElement("a", {
                      className: "App-link",
                      href: "https://reactjs.org",
                      rel: "noopener noreferrer",
                      target: "_blank"
                    }, "Learn Rescript")));
}

var make = App;

So yeah, App is not exported:

Attempted import error: 'App' is not exported from './App.bs'.

So it really seems cumbersome to use as far as I understand ReScript components within JS code. Now maybe with a pure ReScript base it’s better?

I have no issue with make convention at all. cause I mostly import component within rescript too, it’s good to use make as Component if you were importing it to js, it’s make more sense.

How hard would it be to drop the constraint that functions can’t be capitalized? It seems like the kind of thing that will continue to impede adoption over time.

There’s already a workaround for that: let \"Test" = x => x + 1

That seems way better than the make pattern, but…

it sounds like you can’t use that pattern with JSX.

What problem are you trying to solve?

1 Like

I’m trying to understand whether ReScript will ever change to support a more idiomatic pattern for defining React components. The definition I’m using of “idiomatic” is what would things look like if you were using React and JSX with JavaScript.

The introduction to ReScript on the rescript-lang.org says the following:

ReScript looks like JS, acts like JS, and compiles to the highest quality of clean, readable and performant JS, directly runnable in browsers and Node.

I know this doesn’t explicitly mention JSX and React, but reading this I would expect the same principle to apply to JSX.

Ideally I would like the following code to work some day in ReScript:

let Button = (~count: int) => {
    let times = switch count {
    | 1 => "once"
    | 2 => "twice"
    | n => Belt.Int.toString(n) ++ " times"
    }
    let msg = "Click me " ++ times

    <button> {msg} </button>
}

let App = () => {
   <Button count={5} />
}

The reason why I think this is important for ReScript adoption is that this is what people coming from a JavaScript background will likely expect.

I also don’t understand why we currently need to do {msg->React.string} especially when the compiled code looks like return React.createElement("button", undefined, msg);.

1 Like

I think it’s not the right way for ReScript to go. What you want is a JavaScript superset with types, there’s already TypeScript for that. While ReScript looks like JavaScript it’s another language with it’s own concepts of “ideomatic”.

2 Likes

ReScript doesn’t have automatic type coercion like JavaScript, so we need it to make the types match up. You can see this pretty easily by leaving out the React.string call and checking the type error.

There are a number of features that I really like about ReScript that aren’t supported by TypeScript:

  • pattern matching
  • much better type inference
  • labelled function params
  • pipe operator
  • not having to explicitly import/export things from other modules

Why not? What are the benefits of using the make pattern for React components?

  1. No need to duplicate module name in the component name.
  2. It’s a convention to use make for a factory function of a module.
2 Likes

The current syntax for creating React components is the way it is because of the way the syntax sugar (PPX) works. If you don’t use the PPX, you’re not tied to it. In fact, you can use the current ReScript as it is today to create plain function React components (as long as you’re OK with not using JSX). E.g.,

@module("react") @variadic
external h: (
  _,
  Js.Nullable.t<{..}>,
  array<React.element>) => React.element = "createElement"

let \"Hello" = props =>
  h("div", Js.Nullable.null, [
    "Hello, "->React.string,
    props["name"]->React.string,
  ])

let \"DoubleHello" = props =>
  h("div", Js.Nullable.null, [
    h(\"Hello", Js.Nullable.return(props), []),
    h(\"Hello", Js.Nullable.return(props), []),
  ])

This produces exactly the JS you’d expect:

var React = require("react");

function Hello(props) {
  return React.createElement("div", null, "Hello, ", props.name);
}

function DoubleHello(props) {
  return React.createElement("div", null, React.createElement(Hello, props), React.createElement(Hello, props));
}
1 Like

I don’t really have anything new to add here, but just to add myself a data point that I came to ReScript with the same motivation. These features are what drew me in.

Now the builder-function naming convention is a bit of a turn-off for me too, as I feel it introduces unnecessary nesting and syntax nodes. I think it’s not enough to make me go back to TS yet, but it’s the first thing I clearly do not like.

1 Like