Help Converting the Js to ReasonMl

interop

#1

Hey folks,

I have this JS that I want to convert into Reason.

const isBrowser = typeof window !== `undefined`

export const setUser = user => (window.localStorage.gatsbyUser = JSON.stringify(user))

const getUser = () =>
  window.localStorage.gatsbyUser
    ? JSON.parse(window.localStorage.gatsbyUser)
    : {}

export const isLoggedIn = () => {
  if (!isBrowser) return false

  const user = getUser()
  if (user) return !!user.username
}

export const getCurrentUser = () => isBrowser && getUser()

export const logout = callback => {
  if (!isBrowser) return
  setUser({})
  callback()
}

I can’t seem to get my head around this. This is what I have in Reason:


[@bs.val] external browserEnv: bool = "process.browser";

let isBrowser = browserEnv;
Js.log2("isBrowser: ", isBrowser);

/*   This is what is in browser storage:{"email_verified":true,"phone_number_verified":false,"phone_number":"+5555555555","email":"aarmand.inbox@gmail.com","username":"idkjs"} */

let getUser = () => {
  let user = Dom.Storage.(localStorage |> getItem("gatsbyUser"));
  let result = switch user {
  | None => ""
  | Some(user) => user
  }
  
  Js.log(result);
  result;

};
let user = getUser();
// returns
// {"email_verified":true,"phone_number_verified":false,"phone_number":"+5555555555","email":"aarmand.inbox@gmail.com","username":"idkjs"}
Js.log2("USER",user);

Im having a hard time modeling this boolean which is checking if the user is logged in. First we check if we are in the browser and if true, then we check if the user username field exists and if so return true. So this should return true or false

let isLoggedIn = (isBrowser, user) => {
  let user = getUser();
  switch(isBrowser, user) {
    |(false,_) => ()
    |(true, user) => {
      switch user {
      | None => false
      | Some(_u) => true
    }}
  }
};```

Here we want to check if both we are in the browser and if so get back the user.
```re
let getCurrentUser = () => if (isBrowser) {
  let user = getUser();
  /* return the user */
  user;
};
Js.log2("getCurrentUser",getCurrentUser); // returns "getCurrentUser 0"

gist


#2

The Reason code you have now doesn’t exactly match up with the original JavaScript. For example, looking at the JavaScript, the isLoggedIn function should have the following type in Reason: let isLoggedIn: unit => bool.

If we were to do a somewhat literal translation of the JavaScript code, the Reason should look like this:

/* Assuming the following are available:
let isBrowser: bool;
let getUser: unit => Js.nullable({.. "username": Js.nullable(string)}); */

let isLoggedIn() = isBrowser && {
  ()
  |> getUser
  |> Js.Nullable.toOption
  |> Js.Option.andThen((. user) =>
    Js.Nullable.toOption(user##username))
  |> Js.Option.isSome
};

This can definitely be improved further but it’s a good start. The key is to model the data types correctly, especially the type of getUser. Once you know those, you can manipulate the data using the standard functions shipped with BuckleScript to access the possibly-undefined values in a safe way.


#3

I’d do something simple like:

let isBrowser = true; /* Do your logic thing here */

let getUser = () => {
  let user = Dom.Storage.(localStorage |> getItem("gatsbyUser"));
  switch user {
  | None => Js.Obj.empty()
  | Some(user) => user
  }
};

let isLoggedIn = () => {
  if (!isBrowser) {
    false
  } else {
    switch (getUser()) {
      | None => false
      | Some(user) => 
        switch (Js.undefinedToOption(user##username)) {
          | None => false
          | Some(username) => Js.String.length(username) > 0
        }
     }

#4

Thank you both for these excellent explanations.

I can’t seem to find the process.browser api. You guys know where that is? I looked in the node.js process object docs and did some web searches, but not luck. Thanks in advance.


#5

@bsansouci Im trying to work from your lead.

The following code doesnt compile in getUser at | Some(user) => user with the following error:

Error: This expression has type string but an expression was expected of type
         {.. }
string
<root>/src/utils/AuthRe.re

I guess in the None variant you return a Js.Obj.empty() while in Some user is a string. Is this what you intended?

[@bs.val] external browserEnv: bool = "process.browser";

/* let isBrowser = browserEnv; */

let isBrowser = true; /* Do your logic thing here */

let getUser = () => {
  let user = Dom.Storage.(localStorage |> getItem("gatsbyUser"));
  switch user {
  | None => Js.Obj.empty()
  | Some(user) => user
  }
};

let isLoggedIn = () => {
  if (!isBrowser) {
    false
  } else {
    switch (getUser()) {
      | None => false
      | Some(user) =>
        switch (Js.undefinedToOption(user##username)) {
          | None => false
          | Some(username) => Js.String.length(username) > 0
        }
     }
  }
}


#6

@yawaramin not following you, brother.

try this:

[@bs.val] external isBrowser: bool = "process.browser";

let getUser: unit => Js.nullable({.. "username": Js.nullable(string)});

let isLoggedIn() = isBrowser && {
  ()
  |> getUser
  |> Js.Nullable.toOption
  |> Js.Option.andThen((. user) =>
    Js.Nullable.toOption(user##username))
  |> Js.Option.isSome
};

Gettng a syntax error on getUser function. Not really following the style. If i read it out, i see: getUser is a function that returns a Js.nullable mutable (double dots). You have Js.nullable on the whole return value which says we may or may not get this mutable value, then Js.nullable on the username to say even if we get the value, the username may or may not be there. Does that sound correct?

Thank you, sir.


#7

Let’s clarify a couple of different things. First, your isBrowser external is not a direct translation of the JavaScript version. That would look like this:

[@bs.val] external window: _ = "";
let isBrowser = Js.typeof(window) != "undefined";

No need to look for process.browser (which doesn’t actually exist).

Second, you’re getting a syntax error on getUser because what I posted was the type signature of the function, not its actual implementation. You would need to implement getUser yourself. Actually since it’s coming from LocalStorage, the signature should probably be:

let getUser: unit => option({.. "username": Js.nullable(string)});

Edit: since we’re changing the type signature slightly, it will affect the definition of the isLoggedIn function also, we’ll need to get rid of the |> Js.Nullable.toOption line since the getUser function will already be returning an option.

The {.. } type by the way means that it’s an open object type, meaning there might be other fields in the object besides just username. OCaml associates these types with mutability because those other fields might be mutable so it errs on the side of caution. If you wanted, you could flesh out the type a bit more and make it a fully specified, immutable object type e.g.

{.
  "email_verified": Js.nullable(bool),
  "phone_number_verified": Js.nullable(bool),
  "phone_number": Js.nullable(bool),
  "email": Js.nullable(string),
  "username": Js.nullable(string),
}

Depending on how what creates this object, you could have more guarantees about the fields, like some of them may never been undefined or null so you wouldn’t need to use Js.nullable.


#8

You are appreciated @yawaramin, for taking the time to teach me. Thank you, sir. This is a great explanation.

This is strange:
This is my working code:

module Env = {
  [@bs.val] external isBrowser: bool = "process.browser";
  type platform =
    | Browser
    | Node;

  let getPlatform = () => isBrowser ? Browser : Node;
};

let saveIt = (identifier, value) =>
  switch (Env.getPlatform()) {
  | Browser =>
    Dom.Storage.(localStorage |> setItem(identifier, value));
    ();
  | _ => ()
  };

let getStore = identifier =>
  switch (Env.getPlatform()) {
  | Browser => Dom.Storage.(localStorage |> getItem(identifier))
  | _ => None
  };

[@bs.deriving abstract]
type store = {
  mutable username: string,
  mutable email: string,
  mutable phone_number: string,
  mutable phone_number_verified: string,
};
/* reset store values to logout */
let signOut = () =>
  store(~username="", ~email="", ~phone_number="", ~phone_number_verified="");

let store =
  switch (getStore("gatsbyUser")) {
  | Some(storeValues) =>
    try (Js.Json.parseExn(storeValues)->Obj.magic) {
    | exn =>
      Js.log2("Failed to parse store", exn);
      signOut();
    }
  | None => signOut()
  };
let isLoggedIn =
  switch (getStore("gatsbyUser")) {
  | Some(storeValues) =>
    try (Js.Json.parseExn(storeValues)->Obj.magic) {
    | exn =>
      Js.log2("Failed to parse store", exn);
      true;
    }
  | None => false
  };

let saveStore = (): unit => {
  saveIt("gatsbyUser", Js.Json.stringify(store->Obj.magic));
};

I created a random file not accessing the browser:

Js.log("getPlatform()")
Storage.Env.getPlatform() |> Js.log;
Js.log("isBrowser")
Storage.Env.isBrowser |> Js.log;
Js.log("store from node result")
Storage.store |> Js.log;

which outputs:

âžś  utils [master*]node checkBrowser.bs.js
**getPlatform()**
1
**isBrowser**
undefined
**store from node result**
{ username: '',
  email: '',
  phone_number: '',
  phone_number_verified: '' }
âžś  utils [master*]

Then in entry point which access the browser I call same code at top:

Js.log("getPlatform()")
Storage.Env.getPlatform() |> Js.log;
Js.log("isBrowser")
Storage.Env.isBrowser |> Js.log;
Js.log("store from browser result")
Storage.store |> Js.log;

output:

[HMR] Checking for updates on the server... 
**getPlatform()** 
0 
**isBrowser** 
true 
**store from browser result** 
Object { email_verified: true, phone_number_verified: false, phone_number: "+5555555555", email: "aarmand.inbox@gmail.com", username: "idkjs" }

[HMR] Updated modules:

So apparently, process browser is working, right? No wonder i could not find the api, but the code seems to be doing what I want. Your suggestion does the same thing except its backed by a non phantom api!


#9

No problem, but if we look at your original JavaScript:

const isBrowser = typeof window !== `undefined`

That is exactly what I ported over to BuckleScript in my previous message and I personally think it’s a better way because having the window object present tells you with pretty good certainty that this is a browser. Of course since it’s JavaScript you can’t have full certainty of anything, but this is not too bad.