Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

React Renderer Laravel Package

limenius/react-renderer

Limenius ReactRenderer lets PHP applications render React components server-side by delegating to a Node.js process, so you get fast, SEO-friendly markup with props passed from PHP. Includes Symfony bundle integration and Twig helpers for easy embedding.

View on GitHub
Deep Wiki
Context7

Getting Started

limenius/react-renderer is Symfony-first but framework-agnostic at its core — it is a handful of plain PHP classes in Limenius\ReactRenderer\* plus a Node side that executes a webpack-built server bundle. In Laravel you will not get auto-discovery, so plan on ~30 lines of wiring.

1. Know the four things that matter

Namespace What it is
Renderer\ReactRendererInterface render(string $component, array $props, array $options): string — returns HTML
Renderer\PhpExecJsReactRenderer Spawns Node per render (via nacmartin/phpexecjs). Zero infra, slower.
Renderer\ExternalNodeReactRenderer POSTs to a long-running Node HTTP server. Fast, needs supervision.
Context\ContextProviderInterface getContext(): array — data merged into props.context on the server only

Skim README.md for the server-bundle webpack config and the Express example, then read PhpExecJsReactRenderer::render() — that's the whole library.

Pin ^5.0.4. Earlier 5.x releases carry a PHP 8 deprecation (Required parameter $trace follows optional parameter $registeredStores in PhpExecJsReactRenderer.php). It is only a deprecation, not a fatal — but it will spam your logs on every render and it tells you something important about the constructor: there is a $registeredStores parameter sitting in front of $trace. Pass arguments by name, not by position, and this whole class of problem disappears.

2. Build the server bundle (the real first hurdle)

Your resources/js/server.jsx must be a CommonJS build with target: 'node' and libraryTarget: 'commonjs2', and register components via the ReactOnRails API:

// resources/js/server.jsx
import ReactOnRails from 'react-on-rails';
import Dashboard from './components/Dashboard';

ReactOnRails.register({ Dashboard });

With Vite, add a second config (build.ssr: 'resources/js/server.jsx', build.outDir: 'public/js', rollupOptions.output.format: 'cjs'); with Mix use .target('node') and a separate webpack.server.js.

3. Wire it in a service provider

// app/Providers/ReactRendererServiceProvider.php
use Limenius\ReactRenderer\Renderer\ReactRendererInterface;
use Limenius\ReactRenderer\Renderer\PhpExecJsReactRenderer;
use Limenius\ReactRenderer\Context\ContextProviderInterface;

class RequestContext implements ContextProviderInterface
{
    public function __construct(private array $ctx) {}
    public function getContext(): array { return $this->ctx; }
}

$this->app->singleton(ReactRendererInterface::class, fn () => new PhpExecJsReactRenderer(
    bundlePath: base_path('public/js/server-bundle.js'),   // server bundle
    failLoud: app()->isLocal(),                            // $failLoud
    contextProvider: new RequestContext([                  // context provider
        'locale' => app()->getLocale(),
        'path'   => request()->path(),
    ]),
    mode: 'both',                                          // server_side | client_side | both
    trace: false                                           // adds data-* debug attributes
));

Named arguments are not style preference here — the constructor has gained parameters over time ($registeredStores before $trace), so a positional ..., 'both', false can silently bind false to a store registry instead of $trace depending on the exact version. If your Laravel/PHP floor predates PHP 8.0, fall back to positional args but verify the signature of the installed class with ReflectionMethod or by reading vendor/limenius/react-renderer/src/.../PhpExecJsReactRenderer.php directly.

4. First use case: SEO landing page in Blade

{{-- resources/views/welcome.blade.php --}}
{!! app(\Limenius\ReactRenderer\Renderer\ReactRendererInterface::class)
       ->render('Dashboard', ['user' => $user->only('name', 'plan')]) !!}

render() already emits the React-On-Rails wrapper <div data-react-class data-react-props> with the server HTML inside it. Do not wrap it in your own container — client hydration finds it by those attributes. Then in resources/js/app.jsx:

import { createRoot } from 'react-dom/client';
import ReactOnRails from 'react-on-rails';
import Dashboard from './components/Dashboard';

ReactOnRails.register({ Dashboard });

document.querySelectorAll('[data-react-class]').forEach((el) => {
  const Cmp = /* resolve from registry */;
  const props = JSON.parse(el.dataset.reactProps);
  createRoot(el).hydrate(<Cmp {...props} />);
});

Implementation Patterns

Renderer choice drives your whole architecture. PhpExecJsReactRenderer boots Node and re-evaluates the bundle on every render (tens of milliseconds, plus memory). Fine for a few marketing pages, unacceptable for a logged-in dashboard. Move to ExternalNodeReactRenderer early: run the Express server under supervisor/systemd on 127.0.0.1:3000, add a /health route, and restart it on deploy after the new bundle is on disk.

Context is the server/client contract. Server components receive props.context; the client does not get it automatically. Either serialize the same context into the page and merge it in your entrypoint, or write components that degrade gracefully when props.context is undefined:

<script type="application/json" id="react-context">@json($context)</script>
const context = JSON.parse(document.getElementById('react-context').textContent);
// pass into every component as props.context before hydrate

rendering option as a per-component switch. server_side (HTML only, no JS needed), client_side (empty wrapper, hydrate later), both (server HTML + hydration). Use client_side for anything behind auth or with live data; both for marketing/detail pages.

Blade component wrapper keeps views clean:

{{-- resources/views/components/react.blade.php --}}
<div {{ $attributes }}>
    {!! app(\Limenius\ReactRenderer\Renderer\ReactRendererInterface::class)
           ->render($component, $props, ['rendering' => $rendering ?? 'both']) !!}
</div>

Cache aggressively. Server rendering is pure: same component + same props = same HTML. Wrap the renderer in a decorator implementing ReactRendererInterface that hashes the props and checks Redis:

class CachedRenderer implements ReactRendererInterface
{
    public function render($component, $props = [], $options = [])
    {
        $key = 'react:'.md5($component.serialize($props).serialize($options));
        return Cache::remember($key, 3600, fn () => $this->inner->render($component, $props, $options));
    }
}

This is the single highest-leverage optimization and the reason to keep ReactRendererInterface as your injected type everywhere.

Keep props JSON-serializable. No Eloquent models, Carbon objects with timezones, or closures — json_encode will silently drop them. Map to plain arrays in a presenter/view-model. Anything the component needs to fetch must be fetched in PHP first; there is no useEffect pass on the server.

$registeredStores is your Redux seam. The optional parameter ahead of $trace is the hook for passing pre-populated store state into the server bundle. If you use Redux (or a Redux-shaped store), populate it from PHP alongside your props so the server render matches the client's initial state — otherwise you get the classic "server rendered one thing, client rendered another" flash. If you don't use Redux, ignore the parameter entirely.

Progressive enhancement / non-React page sprinkles. This package shines when React is a widget inside a Blade app (a price configurator, a chart, a filter panel) rather than a full SPA. If you're building a whole SPA in Laravel, Inertia SSR is a better fit and this package buys you nothing.

Gotchas and Tips

  • Constructor parameter order is not stable across releases. v5.0.4 fixed a PHP 8 deprecation stemming from $registeredStores (optional) preceding $trace (required) in PhpExecJsReactRenderer.php. Use named arguments; if you must stay positional, read the installed class first. Add a smoke test that instantiates the renderer so a future release can't quietly shift the meaning of your false.
  • failLoud: false silently degrades. When the server bundle throws, you get the wrapper div with no inner HTML and no exception — a blank page that looks like a CSS bug. Keep it true in local/staging, false in production, and log renderer failures in a decorator.
  • Hydration mismatches nuke your HTML. React 18 will log a warning and re-render from scratch if props differ — most often because context was passed on the server but not the client, or a component calls Date.now()/Math.random()/new Date() at render time. Compare the data-react-props JSON with what your entrypoint computes.
  • Server bundle must be window-free at import time. Any module touching document at top level (analytics, jQuery plugins, localStorage) breaks the whole bundle. Guard with typeof window !== 'undefined' or lazy-import inside useEffect.
  • CSS-in-JS is painful. styled-components/Emotion generate <style> tags inside the returned HTML; they work but you must extract and dedupe them server-side, which this package does not do for you. Plain CSS, CSS Modules, or utility classes avoid the problem entirely.
  • trace: true adds data-* attributes to the wrapper — the fastest way to see whether the server or the client produced a given node. Turn it off in production, and remember it's the argument people most often mis-bind when passing positionally.
  • Node version parity. The binary invoked by phpexecjs and the Node running your external server must both be able to require() the bundle. Pin Node in .nvmrc, in the Docker image, and in your deploy pipeline; a bundle built with modern syntax will fail cryptically on an older runtime.
  • Watch the deprecation log after install. Even on 5.0.4, run a single render with E_ALL and tail storage/logs/laravel.log. The PHP 8 "required parameter follows optional" family of warnings is the first symptom of a signature you're calling wrong — catching it at install time beats discovering it via empty divs in production.
  • The renderer never escapes your props for you at the HTML boundary. Props go into data-react-props attribute-escaped, which is fine, but never put user-authored HTML into a prop that a component feeds to dangerouslySetInnerHTML.
  • Old react-on-rails API. The JS side still predates React 18's renderToPipeableStream. Stay on renderToString semantics and call hydrateRoot yourself — don't expect streaming SSR, and don't assume the PHP 8 maintenance implies the Node examples were refreshed.
  • Escape hatch: implement ReactRendererInterface yourself. It's a one-method interface. If the package rots again, swap the binding for an Inertia SSR call, symfony/ux-react, or a hand-rolled Symfony HttpClient POST to your own Express route — your controllers and Blade never notice.
  • Debugging trick: reproduce any failure in isolation without Laravel:
    node -e "const R = require('react-on-rails').default; require('./public/js/server-bundle.js'); \
             console.log(R.serverRenderReactComponent({componentName:'Dashboard', props:{}, domNodeId:'x'}).html)"
    
    If that throws, the problem is your bundle, not the PHP wiring.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons