Skip to content

Fix Inertia SSR errors #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 5, 2025
Merged
5 changes: 5 additions & 0 deletions app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tighten\Ziggy\Ziggy;

class HandleInertiaRequests extends Middleware
{
Expand Down Expand Up @@ -45,6 +46,10 @@ public function share(Request $request): array
'auth' => [
'user' => $request->user(),
],
'ziggy' => fn (): array => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
];
}
}
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
],
"dev:ssr": [
"npm run build",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoeyMckenzie This should run npm run build:ssr instead of npm run build. Go ahead and get that updated and we'll merge this in.

Really appreciate the help!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, updated.

"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr"
]
},
"extra": {
Expand Down
20 changes: 19 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/node": "^22.13.5",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-react": "^7.37.3",
Expand Down
5 changes: 0 additions & 5 deletions resources/js/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@ import '../css/app.css';
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client';
import { route as routeFn } from 'ziggy-js';
import { initializeTheme } from './hooks/use-appearance';

declare global {
const route: typeof routeFn;
}

const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

createInertiaApp({
Expand Down
20 changes: 16 additions & 4 deletions resources/js/hooks/use-appearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@ import { useCallback, useEffect, useState } from 'react';

export type Appearance = 'light' | 'dark' | 'system';

const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersDark = () => {
if (typeof window === 'undefined') {
return false;
}

return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

const applyTheme = (appearance: Appearance) => {
const isDark = appearance === 'dark' || (appearance === 'system' && prefersDark());

document.documentElement.classList.toggle('dark', isDark);
};

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = () => {
if (typeof window === 'undefined') {
return null;
}

return window.matchMedia('(prefers-color-scheme: dark)');
};

const handleSystemThemeChange = () => {
const currentAppearance = localStorage.getItem('appearance') as Appearance;
Expand All @@ -23,7 +35,7 @@ export function initializeTheme() {
applyTheme(savedAppearance);

// Add the event listener for system theme changes...
mediaQuery.addEventListener('change', handleSystemThemeChange);
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
}

export function useAppearance() {
Expand All @@ -39,7 +51,7 @@ export function useAppearance() {
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
updateAppearance(savedAppearance || 'system');

return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
return () => mediaQuery()?.removeEventListener('change', handleSystemThemeChange);
}, [updateAppearance]);

return { appearance, updateAppearance } as const;
Expand Down
5 changes: 5 additions & 0 deletions resources/js/layouts/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const sidebarNavItems: NavItem[] = [
];

export default function SettingsLayout({ children }: PropsWithChildren) {
// For SSR, we can't access the window location, so we only render the layout on the client
if (typeof window === 'undefined') {
return null;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is url in usePage hooks, why not just use that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We certainly could, both will accomplish the same thing I'm pretty sure. This is just a preference of being explicit about rendering on the server.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in that context I prefer that way too, thanks for the answer.


const currentPath = window.location.pathname;

return (
Expand Down
21 changes: 0 additions & 21 deletions resources/js/ssr.jsx

This file was deleted.

30 changes: 30 additions & 0 deletions resources/js/ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createInertiaApp } from '@inertiajs/react';
import createServer from '@inertiajs/react/server';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import ReactDOMServer from 'react-dom/server';
import { type RouteName, route } from 'ziggy-js';

const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

createServer((page) =>
createInertiaApp({
page,
render: ReactDOMServer.renderToString,
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')),
setup: ({ App, props }) => {
/* eslint-disable */
// @ts-expect-error
global.route<RouteName> = (name, params, absolute) =>
route(name, params as any, absolute, {
// @ts-expect-error
...page.props.ziggy,
// @ts-expect-error
location: new URL(page.props.ziggy.location),
});
/* eslint-enable */

return <App {...props} />;
},
}),
);
5 changes: 5 additions & 0 deletions resources/js/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { route as routeFn } from "ziggy-js";

declare global {
const route: typeof routeFn;
}
2 changes: 2 additions & 0 deletions resources/js/types/index.ts → resources/js/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LucideIcon } from 'lucide-react';
import type { Config } from "ziggy-js";

export interface Auth {
user: User;
Expand All @@ -25,6 +26,7 @@ export interface SharedData {
name: string;
quote: { message: string; author: string };
auth: Auth;
ziggy: Config & { location: string };
[key: string]: unknown;
}

Expand Down
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,9 @@
},
"jsx": "react-jsx"
},
"include": ["resources/js/**/*.ts", "resources/js/**/*.tsx"]
"include": [
"resources/js/**/*.ts",
"resources/js/**/*.d.ts",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would "resources/js/**/*.d.ts" not already be targeted with "resources/js/**/*.ts"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is necessary, but I feel like it's not a bad thing to include because it helps with clarity. Some devs perfer to explicitly include the .d.ts 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RV7PR, correct. Technically not necessary, but I like to include them to be very explicit that the project include type declaration files. Similar to how the original Breeze templates worked as well.

"resources/js/**/*.tsx",
]
}
16 changes: 10 additions & 6 deletions vite.config.js → vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
import {
defineConfig
} from 'vite';
import tailwindcss from "@tailwindcss/vite";
import { resolve } from 'node:path';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
ssr: 'resources/js/ssr.jsx',
ssr: 'resources/js/ssr.tsx',
refresh: true,
}),
react(),
Expand All @@ -18,4 +17,9 @@ export default defineConfig({
esbuild: {
jsx: 'automatic',
},
});
resolve: {
alias: {
'ziggy-js': resolve(__dirname, 'vendor/tightenco/ziggy'),
},
},
});