Building Modern SPAs with Yii2 and Inertia.js

5 0
2
Viewed: 55 165 times
Version: 2.0
Category: Tutorials
    Written by: Akshay Joshi Akshay Joshi
    Created: Nov 28, 2025

    You are viewing revision #1 of this wiki article.
    This is the latest version of this article.

    1. Introduction
    2. Installation
    3. Quick Setup
    4. Core Features
    5. Best Practices
    6. Common Patterns
    7. Troubleshooting
    8. Additional Resources
    9. Conclusion

    Introduction

    Inertia.js is a modern approach to building single-page applications (SPAs) without the complexity of building an API. It allows you to use modern JavaScript frameworks like React, Vue, or Svelte while keeping your Yii2 controllers and routing intact.

    Why Inertia.js?

    • No need to build a separate API
    • Keep your existing Yii2 controllers and models
    • Server-side routing and validation
    • Better SEO than traditional SPAs
    • Simpler architecture than API + SPA

    The crenspire/yii2-inertia package provides a seamless Inertia.js adapter for Yii2, matching the developer experience of popular frameworks like Laravel's Inertia adapter.

    Installation

    Install via Composer:

    composer require crenspire/yii2-inertia
    

    Quick Setup

    1. Configure the View Renderer

    Add to your config/web.php:

    'view' => [
        'renderers' => [
            'inertia' => \Crenspire\Yii2Inertia\ViewRenderer::class,
        ],
    ],
    
    2. Create Root Template

    Create views/layouts/inertia.php:

    <!DOCTYPE html>
    <html lang="<?= Yii::$app->language ?>">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title><?= Html::encode($this->title) ?></title>
        <script type="module" src="/dist/assets/index.js"></script>
        <link rel="stylesheet" href="/dist/assets/index.css">
    </head>
    <body>
        <div id="app" data-page="<?= htmlspecialchars(json_encode($page), ENT_QUOTES, 'UTF-8') ?>"></div>
    </body>
    </html>
    
    3. Use in Controllers
    use Crenspire\Yii2Inertia\Inertia;
    
    class HomeController extends \yii\web\Controller
    {
        public function actionIndex()
        {
            return Inertia::render('Home', [
                'title' => 'Welcome',
                'users' => User::find()->all(),
            ]);
        }
    }
    
    4. Setup Frontend

    Install Inertia.js and your framework:

    # For React
    npm install @inertiajs/inertia @inertiajs/inertia-react react react-dom
    
    # For Vue
    npm install @inertiajs/inertia @inertiajs/inertia-vue3 vue
    
    # For Svelte
    npm install @inertiajs/inertia @inertiajs/inertia-svelte svelte
    

    Create src/main.jsx (React example):

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import { createInertiaApp } from '@inertiajs/inertia-react';
    import Home from './pages/Home';
    import Dashboard from './pages/Dashboard';
    
    createInertiaApp({
      resolve: (name) => {
        const pages = { Home, Dashboard };
        return pages[name];
      },
      setup({ el, App, props }) {
        ReactDOM.createRoot(el).render(<App {...props} />);
      },
    });
    

    Core Features

    Sharing Global Data

    Share data that should be available on every page:

    // In config/bootstrap.php or base controller
    use Crenspire\Yii2Inertia\Inertia;
    
    // Share user data
    Inertia::share('user', function () {
        return Yii::$app->user->identity;
    });
    
    // Share flash messages
    Inertia::share('flash', function () {
        return [
            'success' => Yii::$app->session->getFlash('success'),
            'error' => Yii::$app->session->getFlash('error'),
        ];
    });
    
    // Share multiple values
    Inertia::share([
        'appName' => 'My Application',
        'version' => '1.0.0',
    ]);
    

    Tip: Use closures for dynamic data (like user or flash messages) to ensure fresh data on each request.

    Handling Redirects

    For form submissions and other redirects, use Inertia::location():

    public function actionStore()
    {
        $model = new User();
        
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            Yii::$app->session->setFlash('success', 'User created!');
            return Inertia::location('/users');
        }
        
        return Inertia::render('Users/Create', [
            'errors' => $model->errors,
        ]);
    }
    

    The method automatically handles both Inertia requests (409 status) and regular requests (302 redirect).

    Asset Versioning

    Set up versioning for cache busting:

    // Automatic (uses manifest.json mtime if exists)
    $version = Inertia::version();
    
    // Manual string
    Inertia::version('1.0.0');
    
    // Callback (recommended)
    Inertia::version(function () {
        return filemtime(Yii::getAlias('@webroot/dist/manifest.json'));
    });
    
    Partial Reloads

    Optimize performance by only reloading specific props:

    // Client requests only 'users' and 'stats' props
    return Inertia::render('Dashboard', [
        'users' => $users,      // Included
        'stats' => $stats,      // Included
        'other' => $other,      // Excluded
    ]);
    

    In your frontend component:

    import { router } from '@inertiajs/inertia-react';
    
    const refreshStats = () => {
      router.reload({ only: ['stats'] });
    };
    

    Best Practices

    1. Organize Components by Route

    Structure your frontend to match backend routes:

    src/pages/
      Home.jsx
      Dashboard.jsx
      Users/
        Index.jsx
        Show.jsx
        Edit.jsx
    
    // Maps to src/pages/Users/Index.jsx
    return Inertia::render('Users/Index', ['users' => $users]);
    
    2. Handle Forms with Inertia

    Use Inertia's form helper for better UX:

    import { useForm } from '@inertiajs/inertia-react';
    
    export default function CreateUser() {
      const { data, setData, post, processing, errors } = useForm({
        name: '',
        email: '',
      });
    
      const submit = (e) => {
        e.preventDefault();
        post('/users');
      };
    
      return (
        <form onSubmit={submit}>
          <input
            value={data.name}
            onChange={(e) => setData('name', e.target.value)}
          />
          {errors.name && <div>{errors.name}</div>}
          <button disabled={processing}>Submit</button>
        </form>
      );
    }
    
    3. Error Handling

    Return validation errors from your controller:

    public function actionStore()
    {
        $model = new User();
        
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return Inertia::location('/users');
        }
        
        // Return with errors
        return Inertia::render('Users/Create', [
            'errors' => $model->errors,
            'values' => $model->attributes,
        ]);
    }
    
    4. Flash Messages

    Create a reusable flash message component:

    // Share flash messages globally
    Inertia::share('flash', function () {
        return [
            'success' => Yii::$app->session->getFlash('success'),
            'error' => Yii::$app->session->getFlash('error'),
        ];
    });
    
    // FlashMessage.jsx
    import { usePage } from '@inertiajs/inertia-react';
    
    export default function FlashMessage() {
      const { flash } = usePage().props;
      
      if (!flash) return null;
      
      return (
        <div>
          {flash.success && <div className="alert-success">{flash.success}</div>}
          {flash.error && <div className="alert-error">{flash.error}</div>}
        </div>
      );
    }
    
    5. Pagination

    Handle pagination with Yii2's DataProvider:

    use yii\data\ActiveDataProvider;
    
    public function actionIndex()
    {
        $dataProvider = new ActiveDataProvider([
            'query' => User::find(),
            'pagination' => ['pageSize' => 15],
        ]);
        
        return Inertia::render('Users/Index', [
            'users' => $dataProvider->getModels(),
            'pagination' => [
                'currentPage' => $dataProvider->pagination->page + 1,
                'lastPage' => $dataProvider->pagination->pageCount,
                'perPage' => $dataProvider->pagination->pageSize,
                'total' => $dataProvider->totalCount,
            ],
        ]);
    }
    
    6. Optimize Database Queries

    Use eager loading to prevent N+1 queries:

    $users = User::find()
        ->with('posts', 'comments')
        ->all();
    
    7. Minimize Prop Size

    Only send necessary data:

    // Instead of full models
    return Inertia::render('Users/Index', [
        'users' => User::find()
            ->select(['id', 'name', 'email'])
            ->asArray()
            ->all(),
    ]);
    

    Common Patterns

    Base Controller

    Create a base controller for shared functionality:

    namespace app\controllers;
    
    use Crenspire\Yii2Inertia\Inertia;
    use yii\web\Controller;
    
    abstract class BaseController extends Controller
    {
        public function init()
        {
            parent::init();
            
            Inertia::share('user', function () {
                return Yii::$app->user->identity;
            });
        }
        
        protected function inertiaRender($component, $props = [])
        {
            return Inertia::render($component, $props);
        }
    }
    
    Resource Controller
    class UsersController extends BaseController
    {
        public function actionIndex()
        {
            $users = User::find()->all();
            return $this->inertiaRender('Users/Index', ['users' => $users]);
        }
        
        public function actionShow($id)
        {
            $user = User::findOne($id);
            if (!$user) {
                throw new NotFoundHttpException('User not found');
            }
            return $this->inertiaRender('Users/Show', ['user' => $user]);
        }
        
        public function actionStore()
        {
            $model = new User();
            if ($model->load(Yii::$app->request->post()) && $model->save()) {
                Yii::$app->session->setFlash('success', 'User created!');
                return Inertia::location(['users/show', 'id' => $model->id]);
            }
            return $this->inertiaRender('Users/Create', [
                'errors' => $model->errors,
            ]);
        }
    }
    

    Troubleshooting

    Version Mismatch Issues

    If you're experiencing frequent full page reloads:

    1. Ensure your version callback returns a stable value
    2. Check that manifest.json exists and is readable
    3. Verify file permissions
    Inertia::version(function () {
        $manifest = Yii::getAlias('@webroot/dist/manifest.json');
        return file_exists($manifest) ? (string)filemtime($manifest) : '1';
    });
    
    Redirects Not Working

    Always use Inertia::location() instead of Yii::$app->response->redirect() for Inertia requests.

    Props Not Available in Frontend
    1. Verify props are passed as an array in PHP
    2. Ensure props are JSON-serializable (no closures)
    3. Use the usePage() hook correctly:
    import { usePage } from '@inertiajs/inertia-react';
    
    const { props } = usePage();
    const { title, user } = props;
    
    CSRF Token

    Share CSRF token globally:

    Inertia::share('csrfToken', function () {
        return Yii::$app->request->csrfToken;
    });
    

    Use in forms:

    <input type="hidden" name="_token" value={props.csrfToken} />
    

    Additional Resources

    Conclusion

    Inertia.js with Yii2 provides a powerful way to build modern SPAs while keeping the simplicity and power of server-side routing and validation. The crenspire/yii2-inertia package makes it easy to integrate Inertia.js into your Yii2 applications with a familiar API.

    Give it a try and let us know what you think! For issues, questions, or contributions, please visit the GitHub repository.