Guard

<guard> renders its children only when an access check passes. When the check fails it navigates away. It is the component-level counterpart to a route guard.

<Guard> is the same component.

<script>
let isLoggedIn = false
</script>

<button onclick="isLoggedIn = !isLoggedIn">
  {isLoggedIn ? 'Log out' : 'Log in'}
</button>

<!-- redirect is pinned to THIS page on purpose: a guard never navigates to
     the page the visitor is already on, so denial renders nothing here
     instead of sending you somewhere else mid-paragraph. -->
<guard access={isLoggedIn} redirect="/docs/helpers/guard">
  <p>Your account summary goes here.</p>
</guard>
output

access is re-evaluated whenever its dependencies change, so gaining access mounts the children where they stand, and losing it tears the subtree down and sends the visitor to the guard's destination.

The example above pins redirect to this page, which is why logging out leaves an empty space rather than moving you: a guard never navigates to the page the visitor is already on. See below.

What access Accepts

Value Check
access={isLoggedIn} The boolean, used directly.
access="admins" Is that permission name in the app's allow list?
access={['admins', 'editors']} Is any of them in the allow list?
no access at all The app's global access flag, which defaults to allowed.

access="false" in quotes is a string, so it is looked up as a permission name and denied. Only access={false} is boolean false. A valueless <guard access> is an empty string, also denied.

What Happens When It Fails

It navigates. Always - there is no second answer, and the children are never mounted.

Where to is resolved in three steps, first hit wins:

  1. redirect="/login" on the tag.
  2. guards.redirect in config/jaf.config.js, the project-wide default.
  3. /.
source
<guard access={isLoggedIn} redirect="/login">
  <Dashboard />
</guard>

The navigation uses replace: true, so the guarded page never enters the history stack, and it carries the attempted location as ?return= so the destination can send the visitor back. Two details make that safe to rely on:

  • It never loops. A destination that is the page the visitor is already on would be denied again on arrival, so it is not navigated to at all: the guard renders nothing and, in dev, warns naming the guard and the destination.
  • redirect= is a literal path. redirect={somePath} is a build error - the destination is decided before anything renders, so it has to be knowable when the component compiles.

Changed in 0.1. A guard with no redirect used to render pages/errors/403.html in place of its children. It needed a wrapper element to render into, it left the protected page's URL in the address bar, and it made a guard mean two different things. If you kept denial content in a 403 page, move it to the page you redirect to.

See Route Guards for reading ?return= on the destination page, and Error Pages for what 403.html is for now.

Props

Prop Type Description
access boolean | string | string[] The check. Omitted means "use the global access flag".
redirect string A literal path to navigate to when denied. It names where to go, not whether to go. Omitted, the destination falls to guards.redirect in config, then /.

Named Permissions

String and array checks consult an app-wide allow list, seeded from config/guards.config.js if that file exists:

source
// config/guards.config.js
export default {
  access: false,             // global flag when a guard has no access prop
  allow: ['users'],          // permission names granted at startup
  file: '/api/permissions',  // optional: fetched and merged at startup
}

The optional file is fetched without blocking, so a guard that mounts before it resolves sees only the static config.

To change the list at runtime, import guards from jafjs/router - it is not a global, and a bare guards.add(...) in a component script throws.

The allow list is not reactive. A guard written access="admins" does not re-check when the list changes. If a guard's verdict has to change while the page is open, drive it from state you own: access={canEdit}.

<guard> hides UI, it does not protect data. Anything the browser can fetch, a determined user can fetch. Authorise on the server as well.