Search code examples
ffireasonbucklescriptreason-react

How to defined component /binding when using React ref in Reasonml?


I am having issues integrating react-system-notification module in my app, having read the documentation about Reason React Ref I am not sure why the reference is not passed down the stack; a hint would be much appreciated.

I keep getting the error below, I have used this component in the past in React but it seems that there is some issue when used in ReasonML/React. I suspect a null reference is passed down which breaks the component.

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

Check the render method of Notifications.

Binding:

module NotificationSystem = {    
    [@bs.module "react-notification-system"] external reactClass : ReasonReact.reactClass = "default";

    let make = ( children ) => 
    ReasonReact.wrapJsForReason(
        ~reactClass, 
        ~props=Js.Obj.empty(),
        children
    )
};

Component

type action =
  | AddNotification(string);

type state = {
    _notificationSystem: ref(option(ReasonReact.reactRef)),
};

let setNotificationSystemRef = (notificationRef, {ReasonReact.state: state}) => 
  state._notificationSystem := Js.toOption(notificationRef) ;

let component = ReasonReact.reducerComponent("Notifications");

let addNotification = (message, state) => {   
    switch state._notificationSystem^ {
    | None => ()
    | Some(r) => ReasonReact.refToJsObj(r)##addNotification({"message": message, "level": "success"});      
    }
};

let make = (_children) => {
    ...component,
    initialState: () => {_notificationSystem: ref(None) },
    reducer: (action, state) =>
        switch action {
            | AddNotification(message) =>  ReasonReact.SideEffects(((_) => addNotification(message, state)))
        },
    render: ({handle, reduce}) => (
        <div>
            <NotificationSystem ref=(handle(setNotificationSystemRef)) />
            <button onClick=(reduce( (_) => AddNotification("Test Notification Test"))) > (ReasonReact.stringToElement("Click")) </button> 
        </div>
    )
};

Solution

  • After some further investigation, thanks to glensl hint and some messages exchanged on Discord I am posting the complete answer.

    The issue was related to way bsb generated the "require" statement in the javascript output:

    [@bs.module "react-notification-system"] external reactClass : ReasonReact.reactClass = "default";
    

    Was being emitted as:

    var ReactNotificationSystem = require("react-notification-system");
    

    instead of

    var NotificationSystem = require("react-notification-system");
    

    Mightseem a little hacky however I got bsb to emit the correct javascript was using the following statement:

    [@bs.module ] external reactClass : ReasonReact.reactClass = "react-notification-system/dist/NotificationSystem";
    

    Then with some minor tweaking to the wrapper component, I was able to get it working with the following code:

    module ReactNotificationSystem = { [@bs.module ] external reactClass : ReasonReact.reactClass = "react-notification-system/dist/NotificationSystem";

    let make = ( children ) => 
    ReasonReact.wrapJsForReason(
        ~reactClass, 
        ~props=Js.Obj.empty(),
        children
    )
    };
    
    type action =
      | AddNotification(string);
    
    type state = {
        _notificationSystem: ref(option(ReasonReact.reactRef)),
    };
    
    let setNotificationSystemRef = (notificationRef, {ReasonReact.state}) => 
      state._notificationSystem := Js.Nullable.to_opt(notificationRef) ;
    
    let component = ReasonReact.reducerComponent("Notifications");
    
    let addNotification = (message, state) => {   
        switch state._notificationSystem^ {
        | None => ()
        | Some(r) => ReasonReact.refToJsObj(r)##addNotification({"message": message, "level": "success"});      
        }
    };
    
    let make = (_children) => {
        ...component,
        initialState: () => {_notificationSystem: ref(None) },
        reducer: (action, state) =>
            switch action {
                | AddNotification(message) =>  ReasonReact.SideEffects(((_) => addNotification(message, state)))
            },
        render: ({handle, reduce}) => (
        <div>             
            <ReactNotificationSystem ref=(handle(setNotificationSystemRef)) />
            <button onClick=(reduce( (_) => AddNotification("Hello"))) > (ReasonReact.stringToElement("Click")) </button> 
        </div>
      )
    };
    

    A full sample working project can be found on Github here: