Skip to content

Luna: Pages & Props

Introduction

The heart of Luna is a simple contract: a controller returns the name of a page component and the props that component should receive. LaraGram resolves the data on the server; the frontend renders it. This chapter covers how to render pages, how to pass data to them, and the range of prop types Luna offers — from ordinary values to props that load lazily, merge across visits, or persist for a single visit only.

If you haven't yet, read the Luna introduction first.

Rendering Pages

Return a Luna response from any controller with the Luna facade or the luna() helper. The first argument is the component name (relative to resources/js/Pages, without extension); the second is an array of props:

php
use LaraGram\Luna\Luna;

class DashboardController extends Controller
{
    public function index()
    {
        return Luna::render('Dashboard', [
            'stats' => Stat::all(),
        ]);
    }
}

The luna() helper is equivalent and reads well in shorter actions:

php
return luna('Dashboard', ['stats' => Stat::all()]);

Working with the Response

Luna::render() returns a LaraGram\Luna\Response you can further configure before it is sent. The most common methods:

php
return Luna::render('Users/Show', ['user' => $user])
    ->with('key', 'value')      // Add extra props fluently.
    ->rootView('layout')        // Use a non-default root Blade template.
    ->withViewData('meta', $m); // Pass data to the root Blade view (not the component).

withViewData() is for values your root Blade template needs (like an Open Graph tag rendered server-side) — they are not sent to the JS component.

Props

Any JSON-serializable value can be a prop. Eloquent models, collections, and API resources all serialize automatically:

php
return Luna::render('Users/Index', [
    'users' => User::query()
        ->paginate()
        ->through(fn ($user) => [
            'id' => $user->id,
            'name' => $user->name,
        ]),
]);

NOTE

Props are resolved eagerly by default — every closure and query runs on every visit to the page. To defer expensive work, use lazy props, deferred props, or partial reloads.

Lazy (Closure) Props

Wrap a prop in a closure and it is only evaluated when the prop is actually included in the response. On a partial reload that excludes it, the closure never runs:

php
return Luna::render('Users/Index', [
    // Always evaluated.
    'users' => User::all(),

    // Only evaluated when this prop is requested.
    'stats' => fn () => Stat::expensiveAggregate(),
]);

Optional Props

An optional prop is never included on the first visit or on a full reload — it is included only when explicitly requested by a partial reload. Use it for data that is expensive and not needed for the initial render:

php
use LaraGram\Luna\Luna;

return Luna::render('Users/Index', [
    'users' => User::all(),
    'stats' => Luna::optional(fn () => Stat::expensiveAggregate()),
]);

Deferred Props

A deferred prop renders the page immediately without the data, then Luna automatically fetches it in a follow-up request once the page has mounted. This is the easiest way to keep a page fast while still loading secondary data:

php
return Luna::render('Users/Show', [
    'user' => $user,
    'posts' => Luna::defer(fn () => $user->posts),
    'activity' => Luna::defer(fn () => $user->activity, group: 'secondary'),
]);

Props sharing a group are fetched together in a single request; different groups are fetched in parallel requests. Pass rescue: true to keep the page alive if the deferred callback throws, resolving the prop to null instead of failing the visit.

On the client, wrap deferred props in the <Deferred> component to show a fallback while they load:

jsx
import { Deferred } from '@laraxgram/react'

<Deferred data="posts" fallback={<p>Loading posts…</p>}>
    <PostList posts={posts} />
</Deferred>

data may be an array to wait on several deferred props at once. Provide a rescue slot to render when a rescued deferred prop failed. The Vue and Svelte adapters export an equivalent <Deferred> component.

Merging Props

Normally new prop values replace the old ones. A merge prop instead appends/merges into the existing client value — ideal for "load more" and infinite scroll patterns where each visit brings the next page of items:

php
return Luna::render('Feed', [
    'posts' => Luna::merge($nextPage),      // Shallow merge (append arrays).
    'meta' => Luna::deepMerge($metaChunk),  // Recursive deep merge.
]);

Always Props

An always prop is included in every response, even in a partial reload that would otherwise exclude it. Use it for values that must never go stale, such as a flash message or CSRF token:

php
return Luna::render('Users/Index', [
    'users' => User::all(),
    'flash' => Luna::always(fn () => session('flash')),
]);

Once Props

A once prop is resolved a single time and cached for the duration of the request, so multiple resolvers referencing it don't recompute. shareOnce combines this with shared props:

php
Luna::shareOnce('permissions', fn () => auth()->user()->permissions());

Scroll Props (Paginated Data)

A scroll prop wraps paginated data together with the metadata Luna's infinite scroll needs — page numbers, cursors, and whether more data exists:

php
return Luna::render('Feed', [
    'posts' => Luna::scroll($query->paginate(), wrapper: 'data'),
]);

The wrapper names the key that holds the items; Luna manages the surrounding scroll metadata for you.

Shared Data

Often several pages need the same props — the authenticated user, flash messages, unread counts. Rather than repeating them in every controller, share them once, typically from a middleware or service provider. Shared props are merged into every Luna response:

php
use LaraGram\Luna\Luna;

class HandleLunaRequests
{
    public function handle($request, Closure $next)
    {
        Luna::share([
            'auth' => [
                'user' => $request->user()?->only('id', 'name'),
            ],
            'flash' => fn () => session('status'),
        ]);

        return $next($request);
    }
}

Luna::share() accepts a key/value pair, an associative array, or an object implementing ProvidesLunaProperties. Values may be closures (evaluated lazily per request).

Read shared props back on the server with Luna::getShared('auth'), and clear them with Luna::flushShared(). On the client, shared props arrive merged into every page's props and are accessible via the page object — see usePage.

NOTE

Telegram Mini Apps use exactly this mechanism: the telegram middleware shares a validated telegram prop on every response. See Telegram Mini Apps.

Partial Reloads

A partial reload re-requests the current page but asks the server for only a subset of its props. Because lazy and optional props are only evaluated when included, partial reloads are how you selectively refresh data without recomputing everything.

Trigger one from the router with only or except:

js
import { router } from '@laraxgram/react'

// Re-fetch only the `stats` prop; everything else is left untouched.
router.reload({ only: ['stats'] })

// Re-fetch everything except the heavy `report` prop.
router.reload({ except: ['report'] })

The <Link> component accepts the same only / except options, and the usefetch/poll helpers build on partial reloads under the hood.

Loading Props When Visible

The <WhenVisible> component triggers a partial reload for the named prop(s) when the element scrolls into view — perfect for below-the-fold data that pairs with optional props:

jsx
import { WhenVisible } from '@laraxgram/react'

<WhenVisible data="stats" fallback={<Spinner />}>
    <Stats stats={stats} />
</WhenVisible>

Options: buffer (pixels before the element enters the viewport to start loading), as (the wrapper element/tag), always (re-fetch every time it becomes visible, not just once), and params (extra visit options). Vue and Svelte export the same component.

Resetting Merged Props

When paginating with merge props, you sometimes need to discard the accumulated client state (e.g. when a filter changes) and start fresh. Pass reset with the prop keys to clear before merging:

js
router.reload({ only: ['posts'], reset: ['posts'] })

Next Steps

Now that data flows from server to component, learn how to move between pages in Routing & Visits, and how to send data back with Forms.

Released under the MIT License.