CodeIgniter 4 with Inertia.js

If you don’t know about Inertia.js you can learn more about it at its home page https://inertiajs.com/. Its helping you to build single page applications without APIs. In summary the front end React component details shipped with the requested data to the front end avoiding the multiple network request and avoiding the running spinners. Also this helps to improve the pega SEO as the complete page data will be present in the request.

Step 1: Create a CI4 Project or use existing one

First we will create a brand new CodeIgniter 4 project. You can skip this step if you are setting up Inertia on existing project.

# If you need to create a new project
composer create-project codeigniter4/appstarter ci-inertia
cd ci-inertia

# If using existing project, just navigate to it
cd your-existing-project

Step 2: As second step we will inInstall the frontend and backend dependancies

# Frontend dependencies
npm init -y
npm install react react-dom @inertiajs/react @inertiajs/inertia @vitejs/plugin-react 
npm install --save-dev vite laravel-vite-plugin

Step 3: Create a CI4 Inertia adapter

Create a library file at app/Libraries/Inertia.php with below code.

<?php

namespace App\Libraries;

use CodeIgniter\HTTP\ResponseInterface;

class Inertia
{
    protected $viewData = [];
    protected $sharedProps = [];
    protected $rootView = 'app';
    protected $version = null;

    /**
     * Set root template view
     */
    public function setRootView(string $view): self
    {
        $this->rootView = $view;
        return $this;
    }

    /**
     * Share data across all Inertia requests
     */
    public function share($key, $value = null): self
    {
        if (is_array($key)) {
            $this->sharedProps = array_merge($this->sharedProps, $key);
        } else {
            $this->sharedProps[$key] = $value;
        }
        return $this;
    }

    /**
     * Set the asset version
     */
    public function version($version): self
    {
        $this->version = $version;
        return $this;
    }

    /**
     * Render an Inertia response
     */
    public function render(string $component, array $props = []): ResponseInterface
    {
        $response = service('response');
        $request = service('request');

        // Merge shared props with component props
        $props = array_merge($this->sharedProps, $props);

        // Prepare the Inertia payload
        $page = [
            'component' => $component,
            'props' => $props,
            'url' => current_url(),
            'version' => $this->version,
        ];

        // Check if this is an Inertia partial reload
        if ($request->hasHeader('X-Inertia') && $request->getHeaderLine('X-Inertia') === 'true') {
            return $response
                ->setJSON($page)
                ->setHeader('X-Inertia', 'true')
                ->setHeader('Vary', 'Accept')
                ->setStatusCode(200);
        }

        // Load the full page for regular requests
        return $response->setBody(view($this->rootView, [
            'page' => json_encode($page),
            'head' => '' // Ensure head property is always available
        ]));
    }
}

Step 4: Create the base view template

Create a view file app/View/app.php

<?php
// app/Views/app.php
$manifestPath = FCPATH . 'build/manifest.json';
$jsFile = '';
$cssFiles = [];
$isDevelopment = true; // Set to false in production

// Only attempt to parse manifest if the file exists
if (file_exists($manifestPath)) {
    $manifest = json_decode(file_get_contents($manifestPath), true);

    // Find the entry point in the manifest (usually resources/js/app.jsx)
    $entryPoint = 'resources/js/app.jsx';

    if (isset($manifest[$entryPoint])) {
        // Get the generated JS file path
        $jsFile = base_url('build/' . $manifest[$entryPoint]['file']);
        $isDevelopment = false;

        // Extract CSS files if available
        if (isset($manifest[$entryPoint]['css']) && is_array($manifest[$entryPoint]['css'])) {
            foreach ($manifest[$entryPoint]['css'] as $cssFile) {
                $cssFiles[] = base_url('build/' . $cssFile);
            }
        }
    } else {
        // Fallback: look for any entry point
        foreach ($manifest as $key => $value) {
            if (isset($value['isEntry']) && $value['isEntry'] === true) {
                $jsFile = base_url('build/' . $value['file']);
                $isDevelopment = false;

                if (isset($value['css']) && is_array($value['css'])) {
                    foreach ($value['css'] as $cssFile) {
                        $cssFiles[] = base_url('build/' . $cssFile);
                    }
                }
                break;
            }
        }
    }
}

// If we still don't have a JS file, fall back to Vite dev server
if (empty($jsFile)) {
    $jsFile = 'http://localhost:5173/resources/js/app.jsx';
}
?>
<?php if ($isDevelopment): ?>
    <!-- React Refresh Runtime for development only -->
    <script type="module">
        try {
            import('http://localhost:5173/@react-refresh').then(({
                default: RefreshRuntime
            }) => {
                RefreshRuntime.injectIntoGlobalHook(window);
                window.$RefreshReg$ = () => {};
                window.$RefreshSig$ = () => (type) => type;
                window.__vite_plugin_react_preamble_installed__ = true;
            }).catch(e => console.error('React Refresh runtime import failed:', e));
        } catch (e) {
            console.error('React Refresh runtime setup failed:', e);
        }
    </script>
<?php endif; ?>
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Inertia React App</title>

    <!-- CSS files from manifest -->
    <?php foreach ($cssFiles as $css): ?>
        <link rel="stylesheet" href="<?= $css ?>">
    <?php endforeach; ?>

    <!-- Inertia head -->
    <?= $page['head'] ?? '' ?>
</head>

<body>
    <!-- Inertia app div -->
    <div id="app" data-page='<?= $page ?>'></div>

    <?php if ($isDevelopment): ?>
        <!-- Vite dev server script -->
        <script type="module" src="http://localhost:5173/@vite/client"></script>
    <?php endif; ?>

    <!-- JS entry point -->
    <script type="module" src="<?= $jsFile ?>"></script>
</body>

</html>

Step 5: Create Vite service for CI4

Create a file app/Libraries/Vite.php

<?php

namespace App\Libraries;

class Vite
{
    protected $devServerIsRunning = false;
    protected $devServerUrl = 'http://localhost:5173';
    protected $manifestPath = FCPATH . 'build/manifest.json';
    protected $manifest = null;

    public function __construct()
    {
        // Check if dev server is running
        if (ENVIRONMENT === 'development') {
            $this->devServerIsRunning = $this->isDevServerRunning();
        }

        // Load manifest if not in dev mode or dev server is not running
        if (!$this->devServerIsRunning && file_exists($this->manifestPath)) {
            $this->manifest = json_decode(file_get_contents($this->manifestPath), true);
        }
    }

    /**
     * Check if Vite dev server is running
     */
    protected function isDevServerRunning(): bool
    {
        $ch = curl_init($this->devServerUrl);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 1);
        curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        return $statusCode === 200;
    }

    /**
     * Get asset URL for production or development
     */
    public function asset(string $path, string $type = null): string
    {
        if ($this->devServerIsRunning) {
            return $this->devServerUrl . '/' . $path;
        }

        if (!$this->manifest || !isset($this->manifest[$path])) {
            return base_url('/build/' . $path);
        }

        $assetPath = $this->manifest[$path]['file'];
        return base_url('/build/' . $assetPath);
    }

    /**
     * Include the React refresh script in development
     */
    public function reactRefresh(): string
    {
        if (!$this->devServerIsRunning) {
            return '';
        }

        return '<script type="module" src="' . $this->devServerUrl . '/@vite/client"></script>';
    }

    /**
     * Generate all script tags for development or production
     */
    public function scripts(array $entries = ['resources/js/app.jsx']): string
    {
        $html = '';
        
        if ($this->devServerIsRunning) {
            $html .= $this->reactRefresh();
            foreach ($entries as $entry) {
                $html .= '<script type="module" src="' . $this->devServerUrl . '/' . $entry . '"></script>';
            }
        } else {
            foreach ($entries as $entry) {
                if (!$this->manifest || !isset($this->manifest[$entry])) {
                    continue;
                }
                $file = $this->manifest[$entry]['file'];
                $html .= '<script type="module" src="' . base_url('/build/' . $file) . '"></script>';
                
                // Include CSS if there are imports
                if (isset($this->manifest[$entry]['css']) && is_array($this->manifest[$entry]['css'])) {
                    foreach ($this->manifest[$entry]['css'] as $css) {
                        $html .= '<link rel="stylesheet" href="' . base_url('/build/' . $css) . '">';
                    }
                }
            }
        }
        
        return $html;
    }
    
    /**
     * Generate all style tags for development or production
     */
    public function styles(array $entries = ['resources/css/app.css']): string
    {
        $html = '';
        
        if ($this->devServerIsRunning) {
            foreach ($entries as $entry) {
                $html .= '<link rel="stylesheet" href="' . $this->devServerUrl . '/' . $entry . '">';
            }
        } else {
            foreach ($entries as $entry) {
                if (!$this->manifest || !isset($this->manifest[$entry])) {
                    continue;
                }
                $file = $this->manifest[$entry]['file'];
                $html .= '<link rel="stylesheet" href="' . base_url('/build/' . $file) . '">';
            }
        }
        
        return $html;
    }
}

Step 6: Register the services in the app

Update the Services config file at app/Config/Services.php and include the below two methods.

<?php

namespace Config;

use CodeIgniter\Config\BaseService;
use App\Libraries\Inertia;
use App\Libraries\Vite;

/**
 * Services Configuration file.
 *
 * Services are simply other classes/libraries that the system uses
 * to do its job. This is used by CodeIgniter to allow the core of the
 * framework to be swapped out easily without affecting the usage within
 * the rest of your application.
 *
 * This file holds any application-specific services, or service overrides
 * that you might need. An example has been included with the general
 * method format you should use for your service methods. For more examples,
 * see the core Services file at system/Config/Services.php.
 */
class Services extends BaseService
{


    public static function inertia(bool $getShared = true): Inertia
    {
        if ($getShared) {
            return static::getSharedInstance('inertia');
        }

        return new Inertia();
    }

    /**
     * Return the Vite instance
     */
    public static function vite(bool $getShared = true): Vite
    {
        if ($getShared) {
            return static::getSharedInstance('vite');
        }

        return new Vite();
    }
}

Step 7: Create a React application structure

Create a file at resources/js/app.jsx. This will be our root view for inertia based apps. So here we import all the jsx files from Pages directory or inside sub directory of the page directory.

import React from 'react';
import { createRoot } from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/react';

createInertiaApp({
  resolve: (name) => {
    // Import all JSX files from Pages directory
    const pages = import.meta.glob('./Pages/**/*.jsx');
    
    // Try the direct path first (most common case)
    const exactPath = `./Pages/${name}.jsx`;
    if (pages[exactPath]) {
      return pages[exactPath]().then(module => module.default);
    }
    
    // If not found, try to find a matching page with any path structure
    const matchingPaths = Object.keys(pages).filter(path => {
      // Extract component name from path (removes directory structure and extension)
      const componentName = path.split('/').pop().replace(/\.jsx$/, '');
      return componentName === name;
    });
    
    if (matchingPaths.length > 0) {
      return pages[matchingPaths[0]]().then(module => module.default);
    }
    
    // Log available pages in development for debugging
    if (import.meta.env.DEV) {
      console.error(`Page "${name}" not found - available pages:`, 
        Object.keys(pages).map(path => path.split('/').pop().replace(/\.jsx$/, '')));
    }
    
    throw new Error(`Page ${name} not found.`);
  },
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />);
  },
});

Step 8: Create a sample React page component

For testing whether the code we will create two Pages and Link them and see how it works in the front end. First create a file name Home.jsx at app/resouces/js/Page/Home.jsx

import React from 'react';
import { Link } from '@inertiajs/react';

// React 19 component
export default function Home(props) {
  const welcome = props.greeting || 'Welcome to your Inertia.js + React 19 app';
  
  return (
    <div style={{
      maxWidth: '800px',
      margin: '0 auto',
      padding: '2rem',
      textAlign: 'center'
    }}>
      <h1 style={{color: '#4e73df'}}>{welcome}</h1>
      <p>This is your first Inertia.js page with CodeIgniter 4 and React 19.</p>
      
      <div style={{marginTop: '2rem'}}>
        <Link href="/test" style={{
          display: 'inline-block',
          padding: '0.5rem 1rem',
          backgroundColor: '#4c51bf',
          color: 'white',
          borderRadius: '0.25rem',
          textDecoration: 'none'
        }}>
          Go to Test Page
        </Link>
        <h1 className='blue-500'>Bigger Text</h1>
      </div>
    </div>
  );
}

Then create another test page Test.jsx at app/resources/js/Pages/Test.jsx

import React from 'react';
import { Link } from '@inertiajs/react';

export default function Test(props) {
  return (
    <div style={{
      maxWidth: '800px',
      margin: '0 auto',
      padding: '2rem',
      textAlign: 'center'
    }}>
      <h1 style={{color: '#4c51bf'}}>{props.title || 'Test Page'}</h1>
      <p>{props.message || 'This is a test page to demonstrate Inertia.js with CodeIgniter 4.'}</p>
      
      <div style={{marginTop: '2rem'}}>
        <Link href="/" style={{
          display: 'inline-block',
          padding: '0.5rem 1rem',
          backgroundColor: '#4e73df',
          color: 'white',
          borderRadius: '0.25rem',
          textDecoration: 'none'
        }}>
          Back to Home
        </Link>
      </div>
    </div>
  );
}

Step 9: Create a basic CSS file

Create a basic file at resources/css/app.css

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  line-height: 1.6;
  color: #333;
  margin: 0;
  padding: 0;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

h1 {
  color: #3498db;
}

Step 10: Set up Vite configuration

Create a file vite.config.js at root

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
import path from 'path';

export default defineConfig({
  plugins: [
    laravel({
      input: ['resources/js/app.jsx', 'resources/css/app.css'],
      refresh: true,
    }),
    react(),
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './resources/js'),
    },
  },
  build: {
    outDir: 'public/build',
    manifest: true,
    rollupOptions: {
      input: {
        app: 'resources/js/app.jsx',
      },
    },
  },
  server: {
    cors: true,
    strictPort: true,
    port: 5173,
    hmr: {
      host: 'localhost',
    },
  },
});

Step 11: Update package.json with scripts

Update scripts block under the package.json file with sutiable scripts to run the application. We have defined dev, build and preview scripts as shown below.

{
  "name": "ci4-react-app",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@inertiajs/inertia": "^0.11.1",
    "@inertiajs/react": "^1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.0.0",
    "laravel-vite-plugin": "^0.7.4",
    "vite": "^4.3.2"
  }
}

Step 12: Create a sample controller

We will create a Home controller for loading the test pages we have created in above step. So create a file named Home.php at path app/Controllers/Home.php

<?php

namespace App\Controllers;

use App\Controllers\BaseController;

class Home extends BaseController
{
    public function index()
    {
        return service('inertia')->render('Home', [
            'title' => 'Home Page',
            'greeting' => 'Hello from CodeIgniter 4 + React!'
        ]);
    }
    
    public function test()
    {
        return service('inertia')->render('Test', [
            'title' => 'Inertia Test Page',
            'message' => 'This page demonstrates how Inertia.js works with CodeIgniter 4.'
        ]);
    }
}

Step 13: Update routes maps the end points to the newly created controller methods.

Update routes.php file app/Config/Routes.php

<?php

use CodeIgniter\Router\RouteCollection;

/**
 * @var RouteCollection $routes
 */

$routes->get('/', 'Home::index');
$routes->get('/test', 'Home::test');

Step 14: Create a base filter for handling Inertia requests

Update the app/Config/Filters.php class to make inertia filter calls readable

<?php

namespace Config;

use CodeIgniter\Config\Filters as BaseFilters;
use CodeIgniter\Filters\Cors;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\ForceHTTPS;
use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\SecureHeaders;
use App\Filters\InertiaFilter;

class Filters extends BaseFilters
{
    /**
     * Configures aliases for Filter classes to
     * make reading things nicer and simpler.
     *
     * @var array<string, class-string|list<class-string>>
     *
     * [filter_name => classname]
     * or [filter_name => [classname1, classname2, ...]]
     */
    public array $aliases = [
        'csrf'          => CSRF::class,
        'toolbar'       => DebugToolbar::class,
        'honeypot'      => Honeypot::class,
        'invalidchars'  => InvalidChars::class,
        'secureheaders' => SecureHeaders::class,
        'cors'          => Cors::class,
        'forcehttps'    => ForceHTTPS::class,
        'pagecache'     => PageCache::class,
        'performance'   => PerformanceMetrics::class,
        'inertia'       => InertiaFilter::class,
    ];

    /**
     * List of special required filters.
     *
     * The filters listed here are special. They are applied before and after
     * other kinds of filters, and always applied even if a route does not exist.
     *
     * Filters set by default provide framework functionality. If removed,
     * those functions will no longer work.
     *
     * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
     *
     * @var array{before: list<string>, after: list<string>}
     */
    public array $required = [
        'before' => [
            'forcehttps', // Force Global Secure Requests
            'pagecache',  // Web Page Caching
            'inertia', // Add the Inertia filter globally
            
        ],
        'after' => [
            'pagecache',   // Web Page Caching
            'performance', // Performance Metrics
            'toolbar',     // Debug Toolbar
        ],
    ];

    /**
     * List of filter aliases that are always
     * applied before and after every request.
     *
     * @var array<string, array<string, array<string, string>>>|array<string, list<string>>
     */
    public array $globals = [
        'before' => [
            // 'honeypot',
            // 'csrf',
            // 'invalidchars',
        ],
        'after' => [
            // 'honeypot',
            // 'secureheaders',
        ],
    ];

    /**
     * List of filter aliases that works on a
     * particular HTTP method (GET, POST, etc.).
     *
     * Example:
     * 'POST' => ['foo', 'bar']
     *
     * If you use this, you should disable auto-routing because auto-routing
     * permits any HTTP method to access a controller. Accessing the controller
     * with a method you don't expect could bypass the filter.
     *
     * @var array<string, list<string>>
     */
    public array $methods = [];

    /**
     * List of filter aliases that should run on any
     * before or after URI patterns.
     *
     * Example:
     * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
     *
     * @var array<string, array<string, list<string>>>
     */
    public array $filters = [];
}

Run the application

Start the front end application by running the below script we have added to package.json in earlier step.

# Run development server with hot reload
npm run dev

# Run the php server
php spark serve

Additional Steps for Production

When deploying to production, you’ll need to build the React assets:

This will create optimized assets in the public/build directory, which your CodeIgniter application will serve.

How This Works

  1. When a request comes in, the InertiaFilter adds shared data
  2. The controller renders an Inertia response with component name and props
  3. For initial page loads, CI4 sends the full HTML document with the Inertia page data
  4. React hydrates the page on the client side
  5. Subsequent navigation uses Inertia.js to request only the data needed
  6. Hot module replacement is handled by Vite during development

Additional Components You Might Want to Add

  1. Layout Components: Create layout components in React for consistent UI
  2. API Controllers: Separate controllers for API endpoints
  3. Authentication: Implement authentication and share user data
  4. Form Handling: Use Inertia.js form helpers for easier form submissions

I had prepared this document while setting up my existing website to support React with inertia instead of using traditional api based front end. Retested this document by setting a new CodeIgniter 4 projecy and amended this guide to make sure this works for most of the scenario. Hoe ever if you encounter any issues please feel free to post as comments and I will try to answer soon as possible. Also remember ChatGPT can help as well with errors specific to Inertia or Vite configurations.

Leave a Reply

Your email address will not be published. Required fields are marked *