1

What is bucklescript looking for satisfy the Functions are not valid as a React child. error being produced by the following example.

I have this binding to withAuthenticator from aws-amplify-react.

[@bs.deriving abstract]
type props = {
  [@bs.as "Comp"]
  comp: React.element,
  [@bs.optional] includeGreetings: bool,
};

[@genType.import ("aws-amplify-react", "withAuthenticator")] [@react.component]
external make:(
    ~props:props,
  ) => React.element = "withAuthenticator";
let default = make;

In Demo.re I use the binding as follows:

let props = {
  WithAuthenticator.props(
    ~comp={
      <App />;
    },
    ~includeGreetings=true,
    (),
  );
};
Js.log(props);
[@react.component]
let app = () => <WithAuthenticator props />;

Then in App.js I use Demo.re like so:

import Amplify from 'aws-amplify';
import {app as App } from './Demo.bs';
import awsconfig from './aws-exports';
import './App.css';
Amplify.configure(awsconfig);

export default App;

Which produces the following error:

 Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
    in withAuthenticator (created by Demo$app)
    in Demo$app (at src/index.js:7)

I would like to understand what this means in order to deal with when it comes up again.

This is what the compiled bucklescript code is in Demo.bs.js:

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';

var React = require("react");
var App$ReactHooksTemplate = require("./App.bs.js");
var WithAuthenticator$ReactHooksTemplate = require("../aws/WithAuthenticator.bs.js");

var props = {
  Comp: React.createElement(App$ReactHooksTemplate.make, { }),
  includeGreetings: true
};

console.log(props);

function Demo$app(Props) {
  return React.createElement(WithAuthenticator$ReactHooksTemplate.make, {
              props: props
            });
}

var app = Demo$app;

exports.props = props;
exports.app = app;
/* props Not a pure module */

Reproduction of this issue can be found here.

Update:

Here I am trying to follow up on @glennsl's comments/answer below.

// define a type modeling what `withAuthenticator` is expecting
[@bs.deriving abstract]
type props = {
  [@bs.as "Comp"]
  comp: React.element,
  [@bs.optional]
  includeGreetings: bool,
};
// use bs.module instead of gentype
[@bs.module ("aws-amplify-react", "withAuthenticator")]
external withAuthenticator: props => React.component(props) =
  "withAuthenticator";
module AppWithAuthenticator = {
  [@bs.obj]
  external makeProps:
    (~children: 'children, unit) => {. "children": 'children} =
    "";
  let make = props => withAuthenticator(props);
};

This is how it might be used, but doesnt compile.


module AppWithAuth = {
  let props = {
    props(
      ~comp={
        <App />;
      },
      ~includeGreetings=true,
      (),
    );
  };
  [@react.component]
  let make = () => {
    <AppWithAuthenticator props />;
  };
};

compile error:

>>>> Start compiling
[1/3] Building src/aws/AuthenticatorBS-ReactHooksTemplate.cmj

  We've found a bug for you!
  /Users/prisc_000/working/DEMOS/my-app/src/aws/AuthenticatorBS.re 34:6-25

  32 │   [@react.component]
  33 │   let make = () => {
  34 │     <AppWithAuthenticator props />;
  35 │   };
  36 │ };

  This call is missing an argument of type props
glennsl
  • 28,186
  • 12
  • 57
  • 75
armand
  • 693
  • 9
  • 29
  • `withAuthenticator` is a higher-order component constructor. A function that when applied to a component returns another component. You can't use it directly. – glennsl Aug 18 '19 at 22:44
  • Thank you, @glennsl. Can you point me to a resource/example of a binding to a HOC? – armand Aug 18 '19 at 22:56
  • I can't, since I haven't seen any. I'm sure one exists somewhere though, as it doesn't seem like it should be all that hard with the new API. I might be able to look more into it tomorrow if you haven't figured it out by then. – glennsl Aug 18 '19 at 23:01
  • Thank you, sir. Will update if i come up with anything. Looking at some posts now. – armand Aug 18 '19 at 23:19

2 Answers2

2

Something along these lines should work:

[@genType.import ("aws-amplify-react", "withAuthenticator")]
external withAuthenticator : (React.component('a), bool) => React.component('a) = "withAuthenticator";

module AppWithAuthenticator = {
  [@bs.obj]
  external makeProps: (~children: 'children=?, unit) => {. "children": 'children } = "";
  let make = withAuthenticator(App.make, true);
};

ReactDOMRe.renderToElementWithId(<AppWithAuthenticator />, "root");

external withAuthenticator : ... declares the external HOC constructor as a function that takes a react component and a bool, and returns a component that will accept the exact same props due to the 'a type variable being used in both positions.

module AppWithAuthenticator ... applies the HOC constructor to the App component and sets it up so that it can be used with JSX. This is basically the same as importing a react component directly, except we get the external component by way of a function call instead of importing it directly.

Finally, the last line just demonstrates how it could be used.

Note that I obviously haven't tested this properly as I don't have a project set up with aws-amplify and such. I've also never used genType, but it seems pretty straightforward for this use case.

glennsl
  • 28,186
  • 12
  • 57
  • 75
  • when you say it takes the exact same props, is `bool` a prop in this example? Does 'a on the right expect what ever is passed as 'a or 'a and bool? – armand Aug 19 '19 at 12:08
  • No, that's an ordinary function argument. The props are represented by the `'a`, which is expected to be some kind of js object type. It's unfortunately not possible in Reason to extend an object type, but you could hard code the input and output props if you need to. – glennsl Aug 19 '19 at 12:17
  • 1
    `bs.module` or `genType.import` shouldn't make a difference I think. But your `withAuthenticator` binding is very worng. First, it needs to take a component as the first argument, otherwise it's not a HOC at all. Then, as I understand from the docs, you could provide an options object instead of the `bool` argument, which I think is what you're trying to do. But from the documentation I can't see any `Comp` option. There is an [`authenticatorComponents`](https://aws-amplify.github.io/docs/js/authentication#using-withauthenticator-hoc) option however. – glennsl Aug 19 '19 at 13:50
  • And lastly the options object isn't the same as the props object. Those need to be separate. – glennsl Aug 19 '19 at 13:50
  • You could also just bind to the [`Authenticator`](https://aws-amplify.github.io/docs/js/authentication#using-the-authenticator-component) component directly, and skip all this HOC stuff. – glennsl Aug 19 '19 at 13:51
  • Am currently trying to bind it directly, I got the `Comp` from https://github.com/aws-amplify/amplify-js/blob/ece0b229441357b754615052393e09bf8ecf6894/packages/aws-amplify-react/src/Auth/index.jsx#L39 – armand Aug 19 '19 at 13:55
  • 1
    `Comp` there is the component to be wrapped, passed as the first argument. It's not a valid property of the options object. – glennsl Aug 19 '19 at 13:57
  • My binding in the answer should be equivalent to what you're trying to do with the `props` options object, as `includeGreetings` is the second argument. I just simplified it for you. That might have been too simple in the long run though. – glennsl Aug 19 '19 at 13:59
  • thanks for taking the time to teach. Using your example, as is, i get this error: ```( React.component('props), 'props ) => React.element /node_modules/reason-react/src/React.re Error: This expression has type (~children: 'a) => {. "children": 'a} but an expression was expected of type {. }``` – armand Aug 19 '19 at 14:15
  • Yes, sorry, it assumes you want to have some child components. If not, you can either make it optional or just remove it from `makeProps`. – glennsl Aug 19 '19 at 14:23
1

Reason discord channel strikes again. This solution works:

[@bs.module "aws-amplify-react"]
external withAuthenticator:
  // takes a react component and returns a react component with the same signature
  React.component('props) => React.component('props) =
  "withAuthenticator";

module App = {
  [@react.component]
  let make = (~message) => <div> message->React.string </div>;
};

module WrappedApp = {
  include App;
  let make = withAuthenticator(make);
};

And if you want to pass the second includeGreeting prop like in @glennsl's answer:

[@bs.module "aws-amplify-react"]
external withAuthenticator:
  // takes a react component and returns a react component with the same signature
  (React.component('props), bool) => React.component('props) =
  "withAuthenticator";

module App = {
  [@react.component]
  let make = (~message) => <div> message->React.string </div>;
};

module WrappedApp = {
  include App;
  let make = withAuthenticator(make,true);
};

You would call it with:

ReactDOMRe.renderToElementWithId(<WrappedApp message="Thanks" />, "root");

Thanks to @bloodyowl.

And this is what it looks like if you don't use include. See @glennsl's comment below.

module WrappedApp = {
  let makeProps = App.makeProps;
  let make = withAuthenticator(App.make,true);
};
armand
  • 693
  • 9
  • 29
  • 1
    Hmm, clever use of `include App` to "inherit" `makeProps`. But other than this and using `bs.module` instead of `genType.import`, are there other differences? The solutions seem basically equivalent. – glennsl Aug 19 '19 at 18:26
  • I am not qualified to answer that question, brother. The import method is irrelevant. The only difference maybe is that one solution was simple enough for me to understand. I would love to know how your solution would work. What would @bloodyOwl's answer look like using your method? I could not get it to work. I would love add your answer here: https://dev.to/idkjs/binding-to-withauthenticator-in-aws-amplify-react-in-reasonml-52fh – armand Aug 19 '19 at 18:33
  • 1
    It's basically the same, except instead of `include App;` you would define `let makeProps` manually, as if you were importing the component from js. Or if `WrappedApp` should have the exact same props as `App`, you could also just alias `makeProps` from `App`: `let makeProps = App.makeProps;`. That's exactly what `include App;` does as well, except it _also_ includes everything else in `App`. – glennsl Aug 19 '19 at 18:52
  • 1
    I might do a separate Q&A here just on HOCs to explain it more generally and avoid all the noise with `amplify-aws` and such. – glennsl Aug 19 '19 at 18:54
  • I, for one, would greatly appreciate that, sir. The `aws` noise is just because that is the HOC i need to bind so that was the learning vehicle. – armand Aug 19 '19 at 19:05
  • 1
    [Here](https://stackoverflow.com/questions/57641241/how-to-create-binding-and-use-higher-order-component-in-reasonreact) it is. Let me know if anything there is still unclear. – glennsl Aug 24 '19 at 20:24