Getting a value out of a promise

I’m currently building my auth system (yes, from the other post), but I got chained in a Promise.t mess (kinda normal for javascript).

Right now I have this:

fetch(...)
->Promise.then(Response.json)
->Promise.thenResolve(json => {
  let val = JSON.Decode.object(json)->Option.getOrThrow
  // let user = User.fromJson(val)

  Console.log(val)
  // TODO: I should set the state to AuthenticatedWithData(user)
})

But I need that AuthenticatedWithData variant in the outer scope, not in the promise scope. I’m inside of a reducer, so I can’t use any react tricks for it, is there anything I can do to fix that?

Or is there any javascript binding I can write to fix that? I’m kinda confused on that.

1 Like

Easiest solution: Just use await.

You can both use it at the top level or in the body of an async function.
See docs: Async / Await | ReScript Language Manual

let result: User.t = await fetch("")
->Promise.then(Response.json)
->Promise.thenResolve(json => {
  let val = JSON.Decode.object(json)->Option.getOrThrow
  User.fromJson(val)
})

But generally, a good practice is to have a dispatch function trigger a state update of a reducer:

type state = {
  user: option<User.t>,
}

type action = SetUser(User.t)

let reducer = (state, action) => {
  switch action {
  | SetUser(user) => {
      ...state,
      user: Some(user),
    }
  }
}

@react.component
let make = () => {
  let (state, dispatch) = React.useReducer(
    reducer,
    {
      user: None,
    },
  )

  React.useEffect(() => {
    fetch("")
    ->Promise.then(Response.json)
    ->Promise.thenResolve(json => {
      let val = JSON.Decode.object(json)->Option.getOrThrow
      let user = User.fromJson(val)

      dispatch(SetUser(user))
    })
    ->Promise.ignore

    None
  }, [dispatch])

  switch state.user {
  | Some(user) => <div> {React.string("User loaded")} </div>
  | None => <div> {React.string("Loading...")} </div>
  }
}
2 Likes

I’m not in a async context to use await, i’m in a reducer.

About the dispatching inside a promise, it’s a bit more complicated than that. I think I should just put promise everywhere in my code and make everything async by default, dunno.

I’m kinda lost on how to structure my code to begin with. I probably just need more experience with it.

Well in general you should not trigger side effects from inside a reducer. So reducers are usually clean from async functions and they should be that way.

If a reducer action triggers a change in the state, that state can be a dependency of an useEffect for instance, which is where you should trigger your (async) side effects such as fetches.

It also helps to enumerate the possible states of your component:

type state = Loading | Loaded(User.t)

type action = SetState(state)

let reducer = (state, action) =>
  switch action {
  | SetState(state) => state
  }

@react.component
let make = () => {
  let (state, dispatch) = React.useReducer(reducer, Loading)

  React.useEffect(() => {
    fetch("")
    ->Promise.then(Response.json)
    ->Promise.thenResolve(json => {
      let val = JSON.Decode.object(json)->Option.getOrThrow
      let user = User.fromJson(val)

      dispatch(SetState(Loaded(user)))
    })
    ->Promise.ignore

    None
  }, [dispatch]

  switch state {
  | Loaded(user) => <div> {React.string("User loaded")} </div>
  | Loading => <div> {React.string("Loading...")} </div>
  }
}

That said, I very often use a trick to enforce async context when I need it:

module PromiseUtils = {
  let run = (makePromise: unit => promise<unit>) =>
    makePromise()->Promise.ignore
}

let mySyncFn = () =>
  PromiseUtils.run(async () => {
    let json = await fetch("")->Promise.then(Response.json)
    let val = JSON.Decode.object(json)->Option.getOrThrow
    let user = User.fromJson(val)

    dispatch(SetState(Loaded(user)))
  })
1 Like