scalaroutesscala.jsscalajs-react

scalajs-react router. How to perform ajax request inside conditional route


I am trying to make some conditional routes. The condition resolves on the serverside.

Route rule example:

| (dynamicRouteCT("#user" / long.caseClass[User]) ~> dynRender((page: User) => <.div("Hello, " + page.id.toString)))
  .addCondition((page: User) => checkPermissions(page.id))(_ => Some(redirectToPage(Page403)(Redirect.Push)))

checkpermissions body:

  def checkPermissions(id: Long) = CallbackTo.future{
    /*Ajax.get(s"http://some.uri/?id=$id") map (res =>
     * if (something) true
     * else false
     * )
     */

    //the request before returns Future[XMLHttprequest] witch maps to Future[Boolean]
    Future(false)
  }

I got type missmatch here: (page: User) => checkPermissions(page.id)

Is it possible to perform ajax request inside conditional routes?


Solution

  • If we look at def addCondition(cond: Page => CallbackTo[Boolean])(condUnmet: Page => Option[Action[Page]]): Rule[Page] we can see that it requires a CallbackTo[Boolean]. Because of the nature of the JS env, there is now way to go from Future[A] to A. Although it's not a limitation from scalajs-react itself, it is an inherited reality that will affect your scalajs-react code; as this table in the doc shows, there's no way to go from a CallbackTo[Future[Boolean]] to a CallbackTo[Boolean].

    This type-level restriction is actually a really good thing for user experience. The router is synchronous, it must determine how to render routes and route changes immediately. If it were allowed to be async and somehow supported Futures, then the user would experience noticable (and potentially huge) delays without any kind of visual feedback or means of interruption.

    The "right way" to solve this problem is to use a model that covers the async state. This is what I would do:

    1. Create an AsyncState[E, A] ADT with cases: Empty, AwaitingResponse, Loaded(value: A), Failed(error: E).
      (You can enrich these further if desired, eg. loadTime on Loaded, retry callback on Failed, timeStarted on AwaitingResponse, etc.)
    2. Have an instance of AsyncState[Boolean] in your (local/client-side) state.
    3. Optionally kick-off an async load on page startup.
    4. Have the router pass its value to a component and/or check the value of this.
      (The router won't know the value because it's dynamic, use Callback in a for-comprehension to wire things up and satisfy the types.)
    5. Depending on the value of AsyncState[Boolean], render something meaningful to the user. If it's AwaitingResponse, display a little spinner; if it's failed display an error and probably a retry button.

    (It should also be noted that AsyncState[Boolean] shouldn't actually be Boolean as that's not very descriptive or true to the domain. It would probably be something more meaningful like AsyncState[UserAccess] or something like that.)

    Hope that helps! Good luck!