inertia icon indicating copy to clipboard operation
inertia copied to clipboard

CreateInertiaApp resolve runs twice at page first visit

Open killjin opened this issue 2 years ago • 9 comments

Version:

  • @inertiajs/vue3 version: 1.0.8

After we've setup our laravel + inertia app we found out that CreateInertiaApp() runs twice.

We tried to find a solution but haven't found one yet.

app.js

import { createApp, h } from "vue";
import { createInertiaApp } from "@inertiajs/inertia-vue3";
// or import { createInertiaApp } from "@inertiajs/vue3";
import DefaultLayout from "../../views/layouts/default";
import { createPinia } from "pinia";
import { importComponent } from "./import-component.js";
import { ZiggyVue } from "ziggy";
import { Ziggy } from "../../js/ziggy";
import { InertiaProgress } from "@inertiajs/progress";

InertiaProgress.init();
createInertiaApp({
  resolve: (name) => {
    console.log("123");
    const component = importComponent(name);

    // Set default layout if no layout specified on component
    component.default.layout ??= DefaultLayout;
    return component;
  },
  setup({ el, App, props, plugin }) {
    const pinia = createPinia();
    const app = createApp({ render: () => h(App, props) })
      .use(plugin)
      .use(pinia)
      .mixin({ methods: { route: window.route } })
      .use(ZiggyVue, Ziggy);

    // added ziggy route helper
    // shows laravel routes in vue
    app.config.globalProperties.$auth = props.initialPage.props.auth;
    app.mount(el);
  },
});

webpack.mix.js

mix
  .js("resources/assets/js/app.js", "public/js")
  .sass("resources/assets/sass/app.scss", "public/css")
  .webpackConfig(aliasesConfig)
  .sourceMaps()
  .vue({ version: 3 })
  .ziggy()
  .version();

image


I use laravel jetstream, also executed twice. app.js

import './bootstrap';
import '../css/app.css';

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => {
        console.log('resolve', name)
        return resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue'))
    },
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
            .mount(el);
    },
    progress: {
        color: '#4B5563',
    },
});

image

This results in resolving twice, and if resolve returns a different page the first time than the second, test that each page was resolved.

Why? Can you help us?

killjin avatar Jun 19 '23 15:06 killjin

swapSomponent in router.serPage exec as a promise, then app render before reactive component change.

killjin avatar Jun 20 '23 22:06 killjin

I see the same behaviour with inertiajs/react:

"name": "@inertiajs/react", "version": "1.0.8",

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

createInertiaApp({
    resolve: async name => {
        console.log('Resolve', { name });
        return () => <div></div>;
    },
    setup({ el, App, props }) {
        console.log('CreateRoot', { props });
        createRoot(el).render(<App {...props} />)
    },
})

Resolve is logged twice.

haltsir avatar Jun 24 '23 22:06 haltsir

Hey folks! You're right, it's possible for the resolve() callback to be called more than once — especially during the initial page load.

I'm not sure I see any practical issues with that — other than maybe a perceived performance problem, but that's certainly not something I've ever noticed.

The only other possible problem I could see here is if you're somehow using the resolve() callback for other things like page visit tracking or something. If that's the case I'd recommend not doing that and instead using the Inertia events instead: https://inertiajs.com/events

I'm not sure that I want to introduce a caching layer here unless completely necessary (although I appreciate your attempt here @craigrileyuk! 🙏).

Is there another reason why this is an issue that I'm not aware of?

reinink avatar Aug 18 '23 11:08 reinink

I recently faced an issue where the double render caused a flash in the UI, as state in my component differed between first and second renders.

caveat The double render actually pointed out a bug in my code 😳 that I was relying on the second render without noticing, so I should be thanking you instead of reporting an issue. I've fixed it in my own code, so I'm actually ok with the double render now. I'm not sure if it's worth fixing, but figured i'd post the issue I was facing in case it helps someone else, or is important enough to warrant fixing the double render.

The problem arises when you rely on the value of a React ref to make decisions in your component as their state is lagged by 1 render cycle. If you rely on the ref state, the second render can have a different value than the first render since the ref's value is getting hydrated on the first render when being passed a default value.

kinda contrived example:

function SomeComponent({ defaultSearch = '', records } : { defaultSearch: string; records: string[] }){
  const searchRef = useRef<HTMLInputElement>(null);
  const searchValue = searchRef.current?.value;
  
  function search(e: React.ChangeEvent<HTMLInputElement>) {
    // search
  }
  
  return (
      <div>
          <input ref={searchRef} defaultValue={defaultSearch} onChange={search} />
          {records.length === 0 &&
            (searchValue ? (
              <NoResults />
            ) : (
              <EmptyState />
            ))}
      </div>
  );
}

In this example above, when defaultSearch is some truthy string, the UI flashes from the EmptyState to NoResults real quick, because searchValue is undefined on first render, and then takes the value of defaultSearch on the second render.

I was able to fix it by updating the value of searchValue to fallback to defaultSearch, so not a big deal.

function SomeComponent({ defaultSearch, records } : { defaultSearch: string; records: string[] }){
  const searchRef = useRef<HTMLInputElement>(null);
  const searchValue = searchRef.current?.value ?? defaultSearch; // fix was adding defaultSearch here
  
  function search(e: React.ChangeEvent<HTMLInputElement>) {
    // search
  }
  
  return (
      <div>
          <input ref={searchRef} defaultValue={defaultSearch} onChange={search} />
          {records.length === 0 &&
            (searchValue ? (
              <NoResults />
            ) : (
              <EmptyState />
            ))}
      </div>
  );
}

There may be more useful cases for relying on ref being a step behind, and may cause issues for others. Not sure if thats worth pursuing, but figured it was worth mentioning as it took me a bit to figure out why I was seeing a flash.

dbushy727 avatar Oct 27 '23 01:10 dbushy727

Same thing happens with fresh "Laravel 11 + Breeze + React" setup (via the Laravel installer). This isn't a vue-specific issue.

hasan-ozbey avatar Mar 25 '24 16:03 hasan-ozbey

Can confirm that this is not vue specific. I got the same double resolve on a Laravel 11 and React App (using Breeze).

I'm fairly new to React (coming from Vue) so I got a bit lost as to why I was getting refresh loop on a live filter index. (I had a useEffect watching for a data change, with a get())

After looking into React StrictMode, as I thought it was that, the double rendering did help me find a bug and use a ref to track change similarly mentioned by @dbushy727

aihowes avatar May 13 '24 21:05 aihowes