Mob.Router.Hooks (mob v0.9.7)

Copy Markdown View Source

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.

Summary

Functions

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

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

Removes mfa from hook.

Types

hook()

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

mfa_hook()

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

verdict()

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

Functions

rearm_first_render()

@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(hook, mfa)

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

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

unregister(hook, mfa)

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

Removes mfa from hook.