# `Mob.Router.Hooks`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/router/hooks.ex#L1)

Extension points plugins register on the router at runtime — typically
from their `lifecycle.on_start`, which runs before the app's own
`on_start` starts the root screen. Nothing to configure in the app.

## `:before_navigate`

Called with the destination (a screen module or a route atom) before a
navigation that **mounts** a screen — `push_screen`, `reset_to` (either
form). Pops and `pop_to` go back to screens already mounted and don't
call it. The hook returns:

  * `:ok` — navigate as usual.
  * `{:redirect, module}` — mount `module` instead (e.g. a "please
    update" screen), with the same kind of navigation: a push still
    pushes, so BACK returns to the screen underneath.
  * `{:reset, module}` — mount `module` with empty params and replace
    **all** navigation with it, whatever the original action was: every
    stack and tab is discarded, persisted screen state is cleared and the
    history is empty, as `reset_to(socket, module, stack: :all)` does.
    BACK then leaves the app instead of revealing a user screen. If
    `module` fails to resolve or mount, navigation is left untouched and
    the current screen repaints.
  * `{:error, reason}` — refuse: navigation is left untouched and the
    current screen repaints, as for an unknown destination.

Hooks run in the order registered; the first non-`:ok` answer wins. A
hook that raises, exits, or returns anything else counts as
`{:error, _}`. It runs in the router process, so it must be fast when
there's nothing to do — a hook that has to fetch code (mob_deliver's
just-in-time screens) blocks navigation while it does.

## `:after_first_render`

Called once per VM, in its own process, after the app's first frame has
actually been handed to the native layer: the root screen's `render/1`
returned, and `Mob.Sender` committed the resulting tree (`set_root`)
without raising. The router's own first paint is asynchronous, so this is
the first point at which "the app came up" is true rather than hoped for.

A root screen whose `render/1` raises never triggers it; if a restart of
that screen later renders successfully, it fires then. If the root screen
navigates away before its first paint, the first committed frame is the
destination's. Nothing fires in a VM that never commits a frame, including
test routers that don't render (`Mob.Router.start_link/3`).

The module of the screen whose frame was committed is appended to the
hook's arguments (`nil` only while a hot code push has mixed old and new
framework modules). A plugin that needs a frame from one particular
screen — mob_deliver ends an update's probation only on the app's real
root, not on its own "please update" screen — calls `rearm_first_render/0`
from its hook when the screen isn't the one it wants: the next committed
frame, from any screen, fires the hook again.

    Mob.Router.Hooks.register(:before_navigate, {MyPlugin, :before_navigate, []})
    Mob.Router.Hooks.register(:after_first_render, {MyPlugin, :first_render, []})

    # MyPlugin.before_navigate(destination), MyPlugin.first_render(screen_module)

The destination is appended to a `:before_navigate` hook's arguments, and
the committed screen's module to an `:after_first_render` hook's.

# `hook`

```elixir
@type hook() :: :before_navigate | :after_first_render
```

# `mfa_hook`

```elixir
@type mfa_hook() :: {module(), atom(), list()}
```

# `verdict`

```elixir
@type verdict() :: :ok | {:redirect, module()} | {:reset, module()} | {:error, term()}
```

# `rearm_first_render`

```elixir
@spec rearm_first_render() :: :ok
```

Makes `:after_first_render` fire again for the next frame committed, from
any screen, with that screen's module.

For a plugin waiting for a frame from a particular screen: call it from the
hook when the screen that rendered isn't the one you're waiting for. Until
then the hook has fired and stays quiet.

# `register`

```elixir
@spec register(hook(), mfa_hook()) :: :ok
```

Registers `mfa` for `hook`. Registering the same MFA twice is a no-op.

# `unregister`

```elixir
@spec unregister(hook(), mfa_hook()) :: :ok
```

Removes `mfa` from `hook`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
