How to add custom method to ReasonReact component?

495 views Asked by At

I am very new to ReasonML. I am able to successfully create a stateless component with ReasonReact, but I have not figured out how I might add a custom method to the component (e.g. Next.js' static getInitialProps).

When attempting to define the getInitialProps method on the component, I receive a The field getInitialProps does not belong to type ReasonReact.componentSpec error.

How should I add this define this custom method on the React component?

Component

let str = ReasonReact.stringToElement;

let component = ReasonReact.statelessComponent("Index");

let make = (~items: Listings.items, _children) => {
  ...component,
  getInitialProps: () =>
    Js.Promise.(
      Endpoints.fetchListings()
      |> then_(Fetch.Response.json)
      |> then_((json) => Js.Json.decodeArray(json) |> resolve)
    ),
  render: (_self) =>
    <div>
      (List.length(items) > 0 ? <Listings items /> : <Loading />)
    </div>
};

let default =
  ReasonReact.wrapReasonForJs(
    ~component,
    (jsProps) => make(~items=jsProps##items, [||])
  );

Error

We've found a bug for you!
/Users/davidcalhoun/Sites/web/evergreen-roots/pages/Index.re 7:3-17

5 │ let make = (~items: Listings.items, _children) => {
6 │   ...component,
7 │   getInitialProps: () =>
8 │     Js.Promise.(
9 │       Endpoints.fetchListings()

This record expression is expected to have type
  ReasonReact.componentSpec ('a,  'b,  'c,  'd,  'e)
The field getInitialProps does not belong to type ReasonReact.componentSpec
2

There are 2 answers

0
chenglou On BEST ANSWER

Answered in Discord, reposting here:

You can't define arbitrary methods on the component like that. See that ...component? That's spread a record with static fields and updating a few fields like you did, e.g. render.

Also, you can't directly pass a ReasonReact component into a next method, I believe. It's probably asking for a react component class. See the interop section on how to expose the underlying js class for the js side.

In the meantime you can probably just wrap that component in a thin layer of js component that defines the getInitialProps

So you'd have a reactjs component that has getInitialProps, that renders a ReasonReact component that exposed the underlying class through interop. If I'm understanding you right, you'd also write ReasonReact bindings to that js wrapper if you're using that wrapper from Reason. Which makes this process a bit contrived, but you're hitting a pathological case here.

As an side: ideally, the Next.js API would ask for a component class, and a getInitialProps on the side, a totally agnostic function that's not attached onto the component class. That'd greatly simplify the binding process on the Reason side.

0
Adam Lane On

I wanted to get context in my reason-react Next.js page and found a solution. Here is an example of accessing the context to detect if we are rendering on the server and passing a boolean into your ReasonReact component:

let make = (~onServer, _children) => {
/* component code goes here */ 
};

let default =
  ReasonReact.wrapReasonForJs(~component, jsProps => make(~onServer=jsProps##onServer, [||]));

let getInitialProps = context =>
  Js.Promise.make((~resolve, ~reject as _) => {
    let onServer =
      switch (Js.Nullable.toOption(context##req)) {
      | None => false
      | Some(_) => true
      };
    resolve(. {"onServer": onServer});
  });

let inject = [%bs.raw {| (cls, fn) => cls.getInitialProps = fn |}];

inject(default, getInitialProps);

~ https://github.com/zeit/next.js/issues/4202#issuecomment-439175214 All credit to @tmepple and @-rase who posted solution there!

Alternative solution would be to leave your "_app.js" in plain js and pass stuff from that getInitialProps to <Component someParam=manualStuff {...pageProps} /> which can go in as a parameter to your reason-react page.