diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index e211e7b1..aab8fb18 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -82,6 +82,7 @@ body:
- http
- http-guzzle
- inertia
+ - inertia-react
- layout
- log
- log-file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 35f62029..9daf1825 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -70,6 +70,7 @@ body:
- http
- http-guzzle
- inertia
+ - inertia-react
- layout
- log
- log-file
diff --git a/composer.json b/composer.json
index 78950d85..0216e968 100644
--- a/composer.json
+++ b/composer.json
@@ -144,6 +144,10 @@
"type": "path",
"url": "packages/inertia"
},
+ {
+ "type": "path",
+ "url": "packages/inertia-react"
+ },
{
"type": "path",
"url": "packages/layout"
@@ -341,6 +345,7 @@
"marko/hashing": "self.version",
"marko/health": "self.version",
"marko/inertia": "self.version",
+ "marko/inertia-react": "self.version",
"marko/http": "self.version",
"marko/layout": "self.version",
"marko/http-guzzle": "self.version",
@@ -447,6 +452,7 @@
"Marko\\Hashing\\Tests\\": "packages/hashing/tests/",
"Marko\\Health\\Tests\\": "packages/health/tests/",
"Marko\\Inertia\\Tests\\": "packages/inertia/tests/",
+ "Marko\\Inertia\\React\\Tests\\": "packages/inertia-react/tests/",
"Marko\\Http\\Tests\\": "packages/http/tests/",
"Marko\\Http\\Guzzle\\Tests\\": "packages/http-guzzle/tests/",
"Marko\\Layout\\Tests\\": "packages/layout/tests/",
diff --git a/docs/src/content/docs/packages/inertia-react.md b/docs/src/content/docs/packages/inertia-react.md
new file mode 100644
index 00000000..a793b568
--- /dev/null
+++ b/docs/src/content/docs/packages/inertia-react.md
@@ -0,0 +1,106 @@
+---
+title: marko/inertia-react
+description: React companion for marko/inertia - configuration defaults and a frontend marker binding for React.
+---
+
+React companion for [`marko/inertia`](/docs/packages/inertia/) and [`marko/vite`](/docs/packages/vite/). It overlays the parent Inertia configuration with React defaults and registers a frontend marker binding so installing multiple Inertia frontend companions fails loudly.
+
+## Installation
+
+```bash
+composer require marko/inertia-react
+```
+
+Install the matching frontend dependencies in your app:
+
+```bash
+npm install @inertiajs/react react react-dom @vitejs/plugin-react vite
+```
+
+Refer to the [Inertia.js docs](https://inertiajs.com/) for currently supported versions of each frontend adapter.
+
+## Configuration
+
+This package contributes defaults to the parent `config/inertia.php` namespace:
+
+```php title="packages/inertia-react/config/inertia.php"
+return [
+ 'assetEntry' => env('INERTIA_REACT_CLIENT_ENTRY', 'app/react-web/resources/js/app.jsx'),
+];
+```
+
+| Key | Purpose |
+| --- | --- |
+| `assetEntry` | Vite entry used by browser-rendered Inertia responses. |
+
+## Usage
+
+Render React-backed Inertia pages without passing an asset entry; `marko/inertia` reads it from configuration:
+
+```php
+use Marko\Inertia\Inertia;
+use Marko\Routing\Http\Request;
+use Marko\Routing\Http\Response;
+
+class DashboardController
+{
+ public function __construct(
+ private readonly Inertia $inertia,
+ ) {}
+
+ public function index(Request $request): Response
+ {
+ return $this->inertia->render(
+ request: $request,
+ component: 'Dashboard',
+ );
+ }
+}
+```
+
+Create the client entry at `app/react-web/resources/js/app.jsx`:
+
+```jsx title="app/react-web/resources/js/app.jsx"
+import { createInertiaApp } from '@inertiajs/react';
+import { createRoot } from 'react-dom/client';
+
+createInertiaApp({
+ resolve: (name) => {
+ const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
+ return pages[`./Pages/${name}.jsx`];
+ },
+ setup({ el, App, props }) {
+ createRoot(el).render();
+ },
+});
+```
+
+Create the SSR entry at `app/react-web/resources/js/ssr.jsx`:
+
+```jsx title="app/react-web/resources/js/ssr.jsx"
+import { createInertiaApp } from '@inertiajs/react';
+import createServer from '@inertiajs/react/server';
+import ReactDOMServer from 'react-dom/server';
+
+createServer((page) =>
+ createInertiaApp({
+ page,
+ render: ReactDOMServer.renderToString,
+ resolve: (name) => {
+ const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
+ return pages[`./Pages/${name}.jsx`];
+ },
+ setup: ({ App, props }) => ,
+ }),
+);
+```
+
+## API Reference
+
+This package registers `Marko\Inertia\Frontend\InertiaFrontendInterface` to a React marker implementation. Installing more than one Inertia frontend companion produces the same binding conflict protection used by Marko driver siblings.
+
+## Related Packages
+
+- [`marko/inertia`](/docs/packages/inertia/) - renders Inertia responses and handles SSR fallback
+- [`marko/vite`](/docs/packages/vite/) - resolves the configured React Vite entry
+- [`marko/env`](/docs/packages/env/) - provides the `env()` helper used in `config/inertia.php`
diff --git a/docs/src/content/docs/packages/inertia.md b/docs/src/content/docs/packages/inertia.md
index 097e9a25..f28cff54 100644
--- a/docs/src/content/docs/packages/inertia.md
+++ b/docs/src/content/docs/packages/inertia.md
@@ -20,6 +20,7 @@ Configure via `config/inertia.php`:
```php title="config/inertia.php"
return [
'version' => null,
+ 'assetEntry' => null,
'ssr' => [
'enabled' => env('INERTIA_SSR_ENABLED', false),
'url' => env('INERTIA_SSR_URL', 'http://localhost:13714'),
@@ -30,6 +31,7 @@ return [
| Key | Purpose |
| --- | --- |
| `version` | Asset version included in every Inertia page object. Set to `null` to disable version mismatch handling. |
+| `assetEntry` | Default Vite entry used by `render()` when no `$assetEntry` argument is passed. Frontend companion packages (`marko/inertia-react`, `marko/inertia-vue`, `marko/inertia-svelte`) overlay this slot via `config/inertia.php` so installing one swaps the entry without code changes. |
| `ssr.enabled` | When true, the initial browser response attempts to render through the configured Inertia SSR server. |
| `ssr.url` | URL used by the SSR client when SSR is enabled. |
@@ -77,7 +79,13 @@ For a normal browser visit, `render()` returns the initial HTML shell with the p
### Asset Entries
-By default, the HTML shell uses the configured `vite.entry`. Pass an asset entry to target a frontend-specific bundle:
+The Vite entry resolves in this order:
+
+1. The `$assetEntry` argument passed to `render()` (highest priority).
+2. The `inertia.assetEntry` config slot, typically populated by a frontend companion package (`marko/inertia-react`, `marko/inertia-vue`, `marko/inertia-svelte`).
+3. The `vite.entry` fallback from `marko/vite`.
+
+Pass an asset entry per call to override the configured default:
```php
return $this->inertia->render(
diff --git a/packages/inertia-react/.gitattributes b/packages/inertia-react/.gitattributes
new file mode 100644
index 00000000..aa78278f
--- /dev/null
+++ b/packages/inertia-react/.gitattributes
@@ -0,0 +1,5 @@
+/tests export-ignore
+/.github export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/phpunit.xml.dist export-ignore
diff --git a/packages/inertia-react/LICENSE b/packages/inertia-react/LICENSE
new file mode 100644
index 00000000..99a76f97
--- /dev/null
+++ b/packages/inertia-react/LICENSE
@@ -0,0 +1,20 @@
+MIT License
+
+Copyright (c) Devtomic LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/inertia-react/README.md b/packages/inertia-react/README.md
new file mode 100644
index 00000000..6d4845f2
--- /dev/null
+++ b/packages/inertia-react/README.md
@@ -0,0 +1,36 @@
+# marko/inertia-react
+
+React companion for `marko/inertia` - configuration defaults and a frontend marker binding for React.
+
+## Installation
+
+```bash
+composer require marko/inertia-react
+```
+
+## Quick Example
+
+```php
+use Marko\Inertia\Inertia;
+use Marko\Routing\Http\Request;
+use Marko\Routing\Http\Response;
+
+class DashboardController
+{
+ public function __construct(
+ private readonly Inertia $inertia,
+ ) {}
+
+ public function index(Request $request): Response
+ {
+ return $this->inertia->render(
+ request: $request,
+ component: 'Dashboard',
+ );
+ }
+}
+```
+
+## Documentation
+
+Full usage, API reference, and examples: [marko/inertia-react](https://marko.build/docs/packages/inertia-react/)
diff --git a/packages/inertia-react/composer.json b/packages/inertia-react/composer.json
new file mode 100644
index 00000000..ccff5633
--- /dev/null
+++ b/packages/inertia-react/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "marko/inertia-react",
+ "description": "React companion for marko/inertia - configuration and frontend marker binding",
+ "license": "MIT",
+ "type": "marko-module",
+ "require": {
+ "php": "^8.5",
+ "marko/core": "self.version",
+ "marko/env": "self.version",
+ "marko/inertia": "self.version",
+ "marko/vite": "self.version"
+ },
+ "autoload": {
+ "psr-4": {
+ "Marko\\Inertia\\React\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Marko\\Inertia\\React\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "marko": {
+ "module": true
+ }
+ }
+}
diff --git a/packages/inertia-react/config/inertia.php b/packages/inertia-react/config/inertia.php
new file mode 100644
index 00000000..022c6047
--- /dev/null
+++ b/packages/inertia-react/config/inertia.php
@@ -0,0 +1,7 @@
+ env('INERTIA_REACT_CLIENT_ENTRY', 'app/react-web/resources/js/app.jsx'),
+];
diff --git a/packages/inertia-react/module.php b/packages/inertia-react/module.php
new file mode 100644
index 00000000..797abbd1
--- /dev/null
+++ b/packages/inertia-react/module.php
@@ -0,0 +1,12 @@
+ [
+ InertiaFrontendInterface::class => ReactInertiaFrontend::class,
+ ],
+];
diff --git a/packages/inertia-react/src/ReactInertiaFrontend.php b/packages/inertia-react/src/ReactInertiaFrontend.php
new file mode 100644
index 00000000..76a0b984
--- /dev/null
+++ b/packages/inertia-react/src/ReactInertiaFrontend.php
@@ -0,0 +1,15 @@
+toBe('app/react-web/resources/js/app.jsx');
+ expect($config)->not->toHaveKey('ssrEntry');
+});
+
+test('inertia-react binds the inertia frontend marker', function () {
+ $module = require dirname(__DIR__).'/module.php';
+
+ expect($module['bindings'])->toHaveKey(InertiaFrontendInterface::class)
+ ->and($module['bindings'][InertiaFrontendInterface::class])->toBe(ReactInertiaFrontend::class);
+});
+
+test('react inertia frontend identifies itself', function () {
+ expect((new ReactInertiaFrontend())->name())->toBe('react');
+});
+
+test('inertia-react is a marko module', function () {
+ $composer = json_decode(
+ file_get_contents(dirname(__DIR__).'/composer.json'),
+ true,
+ flags: JSON_THROW_ON_ERROR,
+ );
+
+ expect(file_exists(dirname(__DIR__).'/module.php'))->toBeTrue()
+ ->and($composer['extra']['marko']['module'])->toBeTrue()
+ ->and($composer['autoload']['psr-4'])->toHaveKey('Marko\\Inertia\\React\\');
+});
diff --git a/packages/inertia/config/inertia.php b/packages/inertia/config/inertia.php
index a79781b5..11e1145d 100644
--- a/packages/inertia/config/inertia.php
+++ b/packages/inertia/config/inertia.php
@@ -4,6 +4,7 @@
return [
'version' => null,
+ 'assetEntry' => null,
'ssr' => [
'enabled' => env('INERTIA_SSR_ENABLED', false),
'url' => env('INERTIA_SSR_URL', 'http://localhost:13714'),
diff --git a/packages/inertia/src/Frontend/InertiaFrontendInterface.php b/packages/inertia/src/Frontend/InertiaFrontendInterface.php
new file mode 100644
index 00000000..e76246c0
--- /dev/null
+++ b/packages/inertia/src/Frontend/InertiaFrontendInterface.php
@@ -0,0 +1,10 @@
+nullableScalarConfig('inertia.assetEntry');
$props = $this->resolveProps($props, $request, $component);
$page = [
diff --git a/packages/inertia/tests/InertiaTest.php b/packages/inertia/tests/InertiaTest.php
index f96dcf8d..9eece8ed 100644
--- a/packages/inertia/tests/InertiaTest.php
+++ b/packages/inertia/tests/InertiaTest.php
@@ -21,6 +21,7 @@ function createInertia(array $config = [], array $viteConfig = []): Inertia
{
$mergedConfig = new FakeConfigRepository(array_merge([
'inertia.version' => '1.0',
+ 'inertia.assetEntry' => null,
'inertia.ssr.enabled' => false,
'inertia.ssr.url' => 'http://localhost:13714',
'vite.entry' => 'app/web/resources/js/app.js',
@@ -104,6 +105,23 @@ function createInertia(array $config = [], array $viteConfig = []): Inertia
expect($response->body())->not->toContain('app/web/resources/js/app.js');
});
+test('inertia html defaults to the configured inertia asset entry when present', function () {
+ $inertia = createInertia([
+ 'inertia.assetEntry' => 'app/react-web/resources/js/app.jsx',
+ ], [
+ 'entry' => 'app/web/resources/js/app.js',
+ 'devServerUrl' => 'http://localhost:5173',
+ 'devServerStylesheets' => [],
+ 'useDevServer' => true,
+ ]);
+ $request = new Request();
+
+ $response = $inertia->render($request, 'ReactHome');
+
+ expect($response->body())->toContain('http://localhost:5173/app/react-web/resources/js/app.jsx');
+ expect($response->body())->not->toContain('app/web/resources/js/app.js');
+});
+
test('inertia merges shared data with page props', function () {
$inertia = createInertia();
$inertia->share('flash', ['message' => 'Hello']);
diff --git a/packages/inertia/tests/ModuleTest.php b/packages/inertia/tests/ModuleTest.php
index f05f71e8..d202d40f 100644
--- a/packages/inertia/tests/ModuleTest.php
+++ b/packages/inertia/tests/ModuleTest.php
@@ -24,3 +24,12 @@
expect($module)->not->toHaveKey('enabled')
->and($module)->not->toHaveKey('sequence');
});
+
+test('inertia config declares frontend overlay slots', function () {
+ $config = require dirname(__DIR__).'/config/inertia.php';
+
+ expect($config['version'])->toBeNull()
+ ->and($config['assetEntry'])->toBeNull()
+ ->and($config['ssr']['enabled'])->toBeFalse()
+ ->and($config['ssr']['url'])->toBe('http://localhost:13714');
+});