inertiajs / inertia

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
https://inertiajs.com
MIT License
6.01k stars 404 forks source link

Data hydratation issue with partial reload on initial page visit #1890

Open Gregory-Gerard opened 3 weeks ago

Gregory-Gerard commented 3 weeks ago

Version:

Describe the problem:

Implementing lazy loading with Inertia::lazy in Laravel works well for asynchronous data fetching and improves Time To First Byte (TTFB). Using router.visit navigates the application effectively. However, an issue arises when reloading the page: the data fetch initiates correctly but the data itself fails to update in the UI.

Steps to reproduce:

When calling router.reload in a useEffect, we notice that the data is not updated correctly when reloading page using the browser.

// use-partial-reload.ts
import { useEffect } from 'react';
import { router } from '@inertiajs/react';

export default function usePartialReload(only: string[]) {
  useEffect(() => {
    router.reload({
      only,
    });
  }, []);
}

// component.tsx
import React from 'react';
import { Head, usePage } from '@inertiajs/react';
import { usePartialReload } from '@/hooks';

export default function Index() {
  usePartialReload(['contacts']);
  // contacts stay undefined when reloading the page, but get hydrated when using `router.visit`
  const { contacts } = usePage().props;

  return (
    <>
      <Head title="Contacts" />
      {contacts?.data.length}
    </>
  );
}

Unfortunately, this issue has already been raised, for example: https://github.com/inertiajs/inertia/issues/1547

The setTimeout(..., 0) solution still works but something seems off about it.

Debug:

I've tried to do a little debugging and apply some logs to understand what's going on. When the Inertia application is created for the first time, the router.init method is launched, followed by router.handleInitialPageVisit. We can then notice that the setPage method is called, in which the element of interest is this one: https://github.com/inertiajs/inertia/blob/683155c8639aef8dc812c3fab4cdb5e2a35eccde/packages/core/src/router.ts#L469

So I started logging this condition, and indeed, on the first load the visitId changes because the router.reload is called before the router.handleInitialPageVisit. So when we receive the router.reload result, because of this condition, the result is ignored because the visitId has been updated by router.handleInitialPageVisit.

image

A possible solution?

I've never contributed to Inertia, so certainly this solution may have edge cases, but having a bit of fun with the sources I noticed that setPage can take a visitId as a parameter to avoid regenerating it.

So, if I detect that a visitId has already been initialized before the handleInitialPageVisit, I can simply reuse it to avoid regenerating it and ignore the router.reload result.

diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts
--- packages/core/src/router.ts
+++ packages/core/src/router.ts
@@ -83,9 +83,9 @@
   }

   protected handleInitialPageVisit(page: Page): void {
     this.page.url += window.location.hash
-    this.setPage(page, { preserveState: true }).then(() => fireNavigateEvent(page))
+    this.setPage(page, { preserveState: true, visitId: this.visitId ?? undefined }).then(() => fireNavigateEvent(page))
   }

   protected setupEventListeners(): void {
     window.addEventListener('popstate', this.handlePopstateEvent.bind(this))

View source: https://github.com/inertiajs/inertia/blob/683155c8639aef8dc812c3fab4cdb5e2a35eccde/packages/core/src/router.ts#L87

When using this patch, here is the result: CleanShot 2024-06-09 at 19 19 14 And the UI updates well, whether following a router.visit or a browser reload.

If this solution is suitable, I can make a PR with test cases if necessary. In the meantime, I'm keeping the setTimeout(..., 0) which works for the moment. Also, many thanks for this library.

liliangiraudo5 commented 3 weeks ago

I have the same problem !

payalord commented 2 days ago

Looks like I have the same problem, but with router.get and router.visit in useEffect too. In my case I needed to redirect user to /login page from / initially if user is not logged in. So I used `router.get('/login') with useEffect. Probably better to move this logic to backend instead. But still, I were expect router to work in useEffect even initially.