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>
<style>
button {
margin-bottom: 0.5rem;
padding: 0.35rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
</style>
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:
redirect="/login"on the tag.-
guards.redirectinconfig/jaf.config.js, the project-wide default. /.
<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:
// 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.