Nitro

Features

Caching

#[Cached] keeps what a slow mount() or #[Server] method works out, in your application's cache. The next request with the same inputs gets the result without running the method, until time runs out or a save expires it with Nitro::expire().

On this page

A report that counts thousands of rows, a lookup that calls a slow service, a list every visitor sees the same: the work is the same each time, until the data changes. Mark the method #[Cached] and Nitro runs it once, keeps its result, and answers from the cache after. When the data changes, call Nitro::expire() with a tag, and the next request works it out again.

Caching mount()

Put #[Cached] on mount(), with how many seconds to keep the result and the tags that can expire it:

app/Nitro/Pages/Stats/Categories.php Server
<?php

namespace App\Nitro\Pages\Stats;

use App\Models\Post;
use Nitro\Attributes\Cached;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Url;
use Nitro\Component;

class Categories extends Component
{
    #[Url]
    public string $status = 'published';

    /** @var array<string, int> posts by category */
    #[Locked]
    public array $byCategory = [];

    #[Cached(300, tags: ['posts'])]
    public function mount(): void
    {
        $this->byCategory = Post::where('status', $this->status)
            ->selectRaw('category, count(*) as posts')
            ->groupBy('category')
            ->pluck('posts', 'category')
            ->all();
    }
}
  • What is kept. The properties as mount() left them. On the next request, Nitro sets them from the cache and skips mount().
  • Kept by its inputs. A result belongs to what mount() started from: its arguments (the tag's values, or the route's parameters) and the properties before it ran, such as #[Url] values. ?status=graduated is a result of its own.
  • The view still renders. Only mount()'s work is kept. Each request renders the page and signs its state as usual.
  • It works on components too. A component's mount() is kept by the values its tag passes.

Keep a cached mount() to loading properties

On a cache hit, mount() doesn't run, so nothing else it does happens: $this->head(), $this->defer(), Nitro::share() and Nitro::flash() happen only on the request that filled the cache. Give a cached page its title with #[Title], and keep checks such as Gate::authorize() in the route's middleware, which runs on every request. Or cache only the slow part, as below.

Caching only the slow part

When mount() does more than load data, keep the slow part in a method of its own, with Laravel's Cache::remember(), and call it from mount(). The rest of mount() then runs on every request: the check, the head, the shared data.

PHP Server
use Illuminate\Support\Facades\Cache;

public function mount(): void
{
    Gate::authorize('viewStats', Post::class);   // Runs on every request.
    $this->head()->description("Posts by category, {$this->status}");

    $this->byCategory = $this->byCategory($this->status);   // Only this is kept.
}

/** The slow part, kept for five minutes per status. A private helper is never compiled for the browser. */
private function byCategory(string $status): array
{
    return Cache::remember("stats.categories.{$status}", 300, fn () => Post::where('status', $status)
        ->selectRaw('category, count(*) as posts')
        ->groupBy('category')
        ->pluck('posts', 'category')
        ->all());
}

The helper is private and only mount() calls it, so it is never compiled for the browser, and it can use the cache like any Laravel code. You choose its key: put in it everything the result depends on, here the status. Forget it where the data changes:

PHP Server
#[Server]
public function publish(): void
{
    $this->validate();
    Post::create([...$this->form, 'status' => 'published']);

    foreach (Post::STATUSES as $status) {
        Cache::forget("stats.categories.{$status}");
    }
}
`#[Cached]` on `mount()` `Cache::remember()` in a helper
What is kept What mount() or the method leaves in the properties What the helper returns
The rest of mount() Skipped on a hit Runs every time
The key Worked out by Nitro, from the inputs and by Yours to write
Forgetting it Nitro::expire() with a tag, on any store Cache::forget() with its key, or Laravel's cache tags on a store that has them
A change to the class Starts afresh Kept: forget it in your deploy

Why not #[Cached] on the helper?

#[Cached] goes on mount() and #[Server] methods only, because Nitro is the one that calls them, so it can answer from the cache instead. A helper is called by your own code, $this->byCategory(), and Nitro is not in between. On any other method, #[Cached] fails the build.

Forgetting a result

A result is kept for its seconds (60 by default), or until one of its tags expires. Expire a tag where the data changes, such as the #[Server] method that saves:

PHP Server
use Nitro\Facades\Nitro;

#[Server]
public function publish(): void
{
    $this->validate();
    Post::create([...$this->form, 'status' => 'published']);

    Nitro::expire('posts');   // Every result kept under 'posts' is worked out again.
}

Nitro::expire('posts', 'comments') takes any number of tags. Every result kept under one of them, on any page or component, is worked out again on its next request. Results without that tag are left alone. Expiring works on every cache store, including those without tag support, such as file and database.

A deploy that changes the component's class starts afresh too, so a result never outlives the code that made it. A change to another class, such as a model or a service, doesn't: expire its tags in your deploy, or flush the cache.

Caching a #[Server] method

A #[Server] method that loads data, such as a lookup or a search, can be kept the same way. Nitro keeps what it did: the properties it changed and the value it returned. The next call with the same arguments and the same state does the same again, without running the method.

PHP Server
public string $status = 'published';

public string $category = '';

/** @var list<string> */
public array $titles = [];

#[Server]
#[Cached(300, tags: ['posts'], by: ['status'])]
public function inCategory(string $category): void
{
    $this->category = $category;
    $this->titles = Post::where('status', $this->status)
        ->where('category', $category)
        ->latest()
        ->pluck('title')
        ->all();
}

By default, a result is kept by the method's arguments and the whole state. Any property that changed since, even one the method never reads, makes it a new result. by names the properties the result really depends on: here, inCategory('laravel') is found again whatever else the visitor changed, as long as $status is the same. by: [] keeps it by its arguments alone.

On a cache hit
Middleware, boot() , hydrate() Run as usual
#[Validate] rules on the properties the browser changed Checked as usual, before the cache is asked
The method's body Skipped: the properties it changed are set, and its value is returned
$this->dispatch() , redirect() , js() , Nitro::flash() in the method Skipped with the body
A check in the method, such as Gate::authorize() Skipped with the body

Only cache methods that read

A method that saves, sends mail or charges a card must run every time, so it is never #[Cached]. Cache the methods that only load data, and put what must be checked on every call in middleware, boot() or #[Validate] rules, which still run.

One result for each user

A result is shared by every visitor whose request has the same inputs. That is right for a report everyone sees the same, and wrong for anything that depends on who is signed in. Add perUser: true, and each signed-in user gets a result of their own, with one more for guests:

PHP Server
#[Cached(600, tags: ['drafts'], perUser: true)]
public function mount(): void
{
    $this->drafts = auth()->user()->posts()->where('status', 'draft')->count();
}

auth() in a cached method needs perUser

Without perUser, the first user's result is the one everyone gets. If a cached method reads auth(), the session or anything else about the visitor that isn't in its arguments or state, add perUser: true, or don't cache it.

The options

Option Default
seconds (first argument) 60 How long a result is kept. At least 1
tags [] The tags Nitro::expire() forgets it by
perUser false One result for each signed-in user, and one for guests
by the whole state The properties the result depends on ( #[Server] methods and mount() alike). [] for none

Results go to your application's default cache store. To use another one, set it in config/nitro.php:

PHP
// config/nitro.php
'cache' => [
    'store' => 'redis',   // null (the default): the application's default store.
],

What the build refuses

Each of these fails the build, naming the file and line:

  • A browser method. It runs where there is no cache. Mark it #[Server], or load the data in mount().
  • A store's method. A store is made for each request and shared by its components. Cache the mount() or #[Server] method of a component that uses it.
  • A by name that isn't a public property, an empty tag, or fewer than one second.

A result that can't be kept, such as a property holding an object the cache can't store, is an error that names the method. When its inputs can't be told apart, the method simply runs every time. $reload() always runs mount() for fresh values, cached or not.

Next steps