Skip to content

Commit 01805e5

Browse files
JoeyMckenzietnyleataylorotwell
authored
Fix Inertia SSR errors (#53)
* fix: check for window when running ssr * chore: remove ssr files * fix: add ziggy to inertia middleware for ssr * chore: remove bootstrap ssr * chore: add script for ssr * chore: update composer script * Adding solution for Dark Mode flicker with SSR * Adding condition for system appearance and detecting via client-side immediately * feat: add inline style to prevent flash during CSR * formatting * package update --------- Co-authored-by: Tony Lea <tony@devdojo.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent f106eeb commit 01805e5

File tree

18 files changed

+532
-393
lines changed

18 files changed

+532
-393
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\View;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class HandleAppearance
11+
{
12+
/**
13+
* Handle an incoming request.
14+
*
15+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
16+
*/
17+
public function handle(Request $request, Closure $next): Response
18+
{
19+
View::share('appearance', $request->cookie('appearance') ?? 'system');
20+
21+
return $next($request);
22+
}
23+
}

app/Http/Middleware/HandleInertiaRequests.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Foundation\Inspiring;
66
use Illuminate\Http\Request;
77
use Inertia\Middleware;
8+
use Tighten\Ziggy\Ziggy;
89

910
class HandleInertiaRequests extends Middleware
1011
{
@@ -45,6 +46,10 @@ public function share(Request $request): array
4546
'auth' => [
4647
'user' => $request->user(),
4748
],
49+
'ziggy' => fn (): array => [
50+
...(new Ziggy)->toArray(),
51+
'location' => $request->url(),
52+
]
4853
];
4954
}
5055
}

bootstrap/app.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\HandleAppearance;
34
use App\Http\Middleware\HandleInertiaRequests;
45
use Illuminate\Foundation\Application;
56
use Illuminate\Foundation\Configuration\Exceptions;
@@ -13,7 +14,10 @@
1314
health: '/up',
1415
)
1516
->withMiddleware(function (Middleware $middleware) {
17+
$middleware->encryptCookies(except: ['appearance']);
18+
1619
$middleware->web(append: [
20+
HandleAppearance::class,
1721
HandleInertiaRequests::class,
1822
AddLinkHeadersForPreloadedAssets::class,
1923
]);

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
"dev": [
5656
"Composer\\Config::disableProcessTimeout",
5757
"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"
58+
],
59+
"dev:ssr": [
60+
"npm run build:ssr",
61+
"Composer\\Config::disableProcessTimeout",
62+
"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"
5863
]
5964
},
6065
"extra": {

package-lock.json

Lines changed: 371 additions & 353 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"devDependencies": {
1414
"@eslint/js": "^9.19.0",
15+
"@types/node": "^22.13.5",
1516
"eslint": "^9.17.0",
1617
"eslint-config-prettier": "^10.0.1",
1718
"eslint-plugin-react": "^7.37.3",

resources/js/app.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@ import '../css/app.css';
33
import { createInertiaApp } from '@inertiajs/react';
44
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
55
import { createRoot } from 'react-dom/client';
6-
import { route as routeFn } from 'ziggy-js';
76
import { initializeTheme } from './hooks/use-appearance';
87

9-
declare global {
10-
const route: typeof routeFn;
11-
}
12-
138
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
149

1510
createInertiaApp({

resources/js/components/app-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
6363
<SheetHeader className="flex justify-start text-left">
6464
<AppLogoIcon className="h-6 w-6 fill-current text-black dark:text-white" />
6565
</SheetHeader>
66-
<div className="p-4 flex h-full flex-1 flex-col space-y-4">
66+
<div className="flex h-full flex-1 flex-col space-y-4 p-4">
6767
<div className="flex h-full flex-col justify-between text-sm">
6868
<div className="flex flex-col space-y-4">
6969
{mainNavItems.map((item) => (

resources/js/hooks/use-appearance.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,36 @@ import { useCallback, useEffect, useState } from 'react';
22

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

5-
const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches;
5+
const prefersDark = () => {
6+
if (typeof window === 'undefined') {
7+
return false;
8+
}
9+
10+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
11+
};
12+
13+
const setCookie = (name: string, value: string, days = 365) => {
14+
if (typeof document === 'undefined') {
15+
return;
16+
}
17+
18+
const maxAge = days * 24 * 60 * 60;
19+
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
20+
};
621

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

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

13-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
28+
const mediaQuery = () => {
29+
if (typeof window === 'undefined') {
30+
return null;
31+
}
32+
33+
return window.matchMedia('(prefers-color-scheme: dark)');
34+
};
1435

1536
const handleSystemThemeChange = () => {
1637
const currentAppearance = localStorage.getItem('appearance') as Appearance;
@@ -23,23 +44,29 @@ export function initializeTheme() {
2344
applyTheme(savedAppearance);
2445

2546
// Add the event listener for system theme changes...
26-
mediaQuery.addEventListener('change', handleSystemThemeChange);
47+
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
2748
}
2849

2950
export function useAppearance() {
3051
const [appearance, setAppearance] = useState<Appearance>('system');
3152

3253
const updateAppearance = useCallback((mode: Appearance) => {
3354
setAppearance(mode);
55+
56+
// Store in localStorage for client-side persistence...
3457
localStorage.setItem('appearance', mode);
58+
59+
// Store in cookie for SSR...
60+
setCookie('appearance', mode);
61+
3562
applyTheme(mode);
3663
}, []);
3764

3865
useEffect(() => {
3966
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
4067
updateAppearance(savedAppearance || 'system');
4168

42-
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
69+
return () => mediaQuery()?.removeEventListener('change', handleSystemThemeChange);
4370
}, [updateAppearance]);
4471

4572
return { appearance, updateAppearance } as const;

resources/js/layouts/settings/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const sidebarNavItems: NavItem[] = [
2525
];
2626

2727
export default function SettingsLayout({ children }: PropsWithChildren) {
28+
// When server-side rendering, we only render the layout on the client...
29+
if (typeof window === 'undefined') {
30+
return null;
31+
}
32+
2833
const currentPath = window.location.pathname;
2934

3035
return (

0 commit comments

Comments
 (0)