inertia icon indicating copy to clipboard operation
inertia copied to clipboard

SSR for React; adapter differences between React + Vue

Open bobbypiper opened this issue 3 years ago • 11 comments

Discussed in https://github.com/inertiajs/inertia/discussions/1081

Originally posted by ZeoKnight February 4, 2022 Having read @aarondfrancis comments here https://github.com/tighten/ziggy/issues/431 and useful blog post https://aaronfrancis.com/2022/using-ziggy-with-inertia-server-side-rendering for dealing with global scope window issues.

How do we approach this in React? the features available within the vue adaptor simply aren't present in the react adaptor?

#Vue adaptor
import {createInertiaApp} from '@inertiajs/inertia-vue3'
createServer((page) => createInertiaApp({
    page,
    render: renderToString,
    resolve: name => require(`./Pages/${name}`),
    setup({app, props, plugin}) {
        const Ziggy = {
            // Pull the Ziggy config off of the props.
            ...props.initialPage.props.ziggy,
            // Build the location, since there is
            // no window.location in Node.
            location: new URL(props.initialPage.props.ziggy.url)
        }
 
        return createSSRApp({
            render: () => h(app, props),
        }).use(plugin).mixin({ 
            methods: {
                route: (name, params, absolute, config = Ziggy) => route(name, params, absolute, config),
            },
        })
    },
}))

#React Adaptor
import {createInertiaApp} from '@inertiajs/inertia-react'
createServer((page) => createInertiaApp({
  page,
  render: ReactDOMServer.renderToString,
  resolve: name => require(`./Pages/${name}`),
  setup: ({ App, props }) => <App {...props} />,
}))

Note the setup method does not have plugins or uses a helper method createSSRApp - react setup simply returns the component + props.

bobbypiper avatar Feb 04 '22 12:02 bobbypiper

Hey I'm glad that post was helpful! Sadly, I know less than nothing about React, otherwise I'd love to help you out here 😂

aarondfrancis avatar Feb 04 '22 17:02 aarondfrancis

I am also stuck at this point @ZeoKnight. Were you able to figure it out? Please let me know.

vampiregrey avatar Feb 04 '22 23:02 vampiregrey

@ZeoKnight @aarondfrancis I am not sure if this is the right approach but this worked for me.

import React, {useState} from 'react'
import ReactDOMServer from 'react-dom/server'
import { createInertiaApp } from '@inertiajs/inertia-react'
import createServer from '@inertiajs/server'
import route from "ziggy-js";

createServer((page) => createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: name => require(`./Pages/${name}`),
    setup: ({ App, props }) => {
        const Ziggy = {
            // Pull the Ziggy config off of the props.
            ...props.initialPage.props.ziggy,
            // Build the location, since there is
            // no window.location in Node.
            location: new URL(props.initialPage.props.ziggy.url)
        }

        global.route = (name, params, absolute, config = Ziggy) => route(name, params, absolute, config);

        return <App {...props} />
    },
}))

vampiregrey avatar Feb 05 '22 01:02 vampiregrey

@vampiregrey fantastic stuff! didn't know that global functioned like that.

Only problem I have now is I cannot use route() directly in my react files; errors with:

TypeError: Cannot read property 'posts.index' of undefined

for now, I've been able to replace all calls to route() to global.route() which works perfectly... but feels less than ideal.

side note, does anybody else have an issue with ziggy-js package autocomplete in VSCode? it's never able to find the package correctly for me - have to manually import it (other packages are fine)

bobbypiper avatar Feb 05 '22 11:02 bobbypiper

I've altered my apprach here slightly; the setup method within my ssr.js file now looks like this:

  setup: ({ App, props }) => {
    global.ziggyConfig = {
      ...props.initialPage.props.ziggy,
      location: new URL(props.initialPage.props.ziggy.url),
    };
    return <App {...props} />;
  },

I've then created a thin "routeProxy" method:

import route from 'ziggy-js';

function routeProxy(name, params, absolute = true) {
  return route(name, params, absolute, global.ziggyConfig);
}

export default routeProxy;

which allows me to force the ziggy config from the global value; without having to use global.route everywhere in my app.

bobbypiper avatar Feb 05 '22 14:02 bobbypiper

I'll update my post to point back to this issue for the React solution. Nice work getting it sorted!

aarondfrancis avatar Feb 05 '22 15:02 aarondfrancis

I hope this would help others.

What I've done is generate the ziggy.js file with php artisan ziggy:generate then import the generated js.

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { createInertiaApp } from '@inertiajs/inertia-react'
import createServer from '@inertiajs/server'
import route from "ziggy-js";
// Import generated ziggy.js
import { Ziggy } from '@/ziggy'


createServer((page) => createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: name => require(`./Pages/${name}`),
    setup: ({ App, props }) => {

        // Set global function route
        global.route = (name, params, absolute, config = Ziggy) => route(name, params, absolute, config);

        return <App {...props} />
    },
}))

JefteCaro avatar Feb 14 '22 09:02 JefteCaro

To offer another solution: I followed parts of @aarondfrancis his blog post but swapped out the end for this:

createServer(page => {
    globalThis.Ziggy = page.props.ziggy;

    return createInertiaApp({
        page,
        render: ReactDOMServer.renderToString,
        resolve: name => require(`./Pages/${name}`).default,
        setup: ({ App, props }) => <App {...props} />,
    });
});

You see, ziggy by default already tries to find a ziggy instance in globalThis (see https://github.com/tighten/ziggy/blob/b3ebdd831a9da9dd9839c611f7aff7d82d1bf159/src/js/Router.js#L17). This allows me to use the regular route function from ziggy without having to write a wrapper. Hope it helps someone!

rdgout avatar Feb 28 '22 12:02 rdgout

@rdgout Best solution so far for my case! Thank you very much 😄

JelleDev avatar Mar 03 '22 09:03 JelleDev

Currently, I meet another problem if I render a loop with route, it will fail.

Example like let's say I have a basic action:

Route::get('/posts', function () {
    $posts = \App\Models\Post::all();

    return Inertia::render('Posts', [
        'posts' => $posts,
    ]);
});

Then, The SSR can not render route in the loop

import React from 'react';
import useRoute from '@/Hooks/useRoute';
import useTypedPage from '@/Hooks/useTypedPage';
import type { Post } from '@/types';

export default function Posts() {
  const route = useRoute();
  const page = useTypedPage<{
    posts: Post[];
  }>();
  const { posts } = page.props;

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <div>{post.title}</div>
          <div>{route('posts.show', post.id)}</div>
        </div>
      ))}
    </div>
  );
}

And route().current() is missing.

UPDATE

Sorry for post a wrong issue. Because I use laravel-jetstream-react. .

It's useRoute have skip excute route() if there is no window. After I change source code. It works. Thanks

andyyou avatar Jun 04 '22 07:06 andyyou

anyone had this issue before:

createServer(
^

TypeError: createServer is not a function
    at file:///Users/hass/Sites/iqdam/medialake/public/build/ssr.js:9929:1
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:541:24)
    at async loadESM (node:internal/process/esm_loader:91:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12)

here is my ssr.ts

import { createSSRApp, h } from 'vue';
import { createInertiaApp, Head, Link } from '@inertiajs/inertia-vue3';
import { resolvePageComponent } from 'vite-plugin-laravel/inertia';
import { createPinia } from 'pinia';
import AppLayout from '@/Layouts/AppLayout.vue';
import createServer from '@inertiajs/server';
import { renderToString } from '@vue/server-renderer';

createServer((page) =>
	createInertiaApp({
		page,
		render: renderToString,
		resolve: async (name) => {
			const page = await resolvePageComponent(name, import.meta.globEager('../views/Pages/**/*.vue'));
			page.layout = page.layout || AppLayout;

			return page;
		},
		setup({ app, props, plugin }) {
			return createSSRApp({ render: () => h(app, props) })
				.use(plugin)
				.use(createPinia())
				.component('Link', Link) // Register Link globally, so we don't have to import in every file
				.component('Head', Head) // Register Head globally, so we don't have to import in every file
				.mixin({
					// @ts-ignore
					methods: { route },
					computed: {
						$log: () => console.log
					}
				});
		}
	})
);

azimidev avatar Sep 08 '22 16:09 azimidev

Hey! Thanks so much for your interest in Inertia.js and for sharing this issue/suggestion.

In an attempt to get on top of the issues and pull requests on this project I am going through all the older issues and PRs and closing them, as there's a decent chance that they have since been resolved or are simply not relevant any longer. My hope is that with a "clean slate" me and the other project maintainers will be able to better keep on top of issues and PRs moving forward.

Of course there's a chance that this issue is still relevant, and if that's the case feel free to simply submit a new issue. The only thing I ask is that you please include a super minimal reproduction of the issue as a Git repo. This makes it much easier for us to reproduce things on our end and ultimately fix it.

Really not trying to be dismissive here, I just need to find a way to get this project back into a state that I am able to maintain it. Hope that makes sense! ❤️

reinink avatar Jul 28 '23 01:07 reinink

In my own case, I was getting the error: ReferenceError: route is not defined even after trying out all the suggestions above. This was because in my app, I use the helper route('name') to create all my URLs. What I did to fix it was to add route to the globalThis module, which makes the route helper available for Node.js.

This works for me:

createServer((page) => {
    globalThis.route<RouteName> = (name, params, absolute) =>
        route(name, params, absolute, {
            ...page.props.ziggy,
            location: new URL(page.props.ziggy.location),
        });

    return createInertiaApp({
        page,
        render: ReactDOMServer.renderToString,
        title: (title) => `${title} - ${appName}`,
        resolve: async (name) => {
            const pages = import.meta.glob("./Pages/**/*.tsx");
            let page: any = pages[`./Pages/${name}.tsx`];

            if (typeof page === "function") {
                page = await page();
            }

            if (typeof page === "undefined") {
                throw new Error(`Page not found: ${name}`);
            }

            if (!page.default.layout) {
                const publicPageFolders = ["Public/"];

                const isPublicPage = publicPageFolders.some((folder) =>
                    name.startsWith(folder),
                );

                if (isPublicPage) {
                    page.default.layout = (page: any) => (
                        <GuestLayout children={page} />
                    );
                }
            }

            return page;
        },
        setup: ({ App, props }) => {
            return <App {...props} />;
        },
    });
});

ttebify avatar Jun 26 '24 10:06 ttebify

Any idea if there will be an official approach to this? I'm using Inertia - React - SSR and Jetstream but can't seem to get any of the suggestions working.

phil-hudson avatar Jul 01 '24 10:07 phil-hudson