1

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>
    )
};
glennsl
  • 28,186
  • 12
  • 57
  • 75
user465374
  • 1,521
  • 4
  • 20
  • 39

2 Answers2

1

My guess would be that react-notification-system is not distributed as an es6 component, and therefore does not export default. Try removing default from the external:

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

You should always start by trying out the simplest implementation first, then build incrementally from there, to minimize possible causes of errors. Especially when dealing with something as error-prone as the js boundary. In this case that would be without the complex ref handling. You'll likely find that it still does not work, because of the above, and that you've been looking in the wrong place because you bit off more than you can chew.

glennsl
  • 28,186
  • 12
  • 57
  • 75
1

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:

user465374
  • 1,521
  • 4
  • 20
  • 39