Next.js Discord

Discord Forum

Router.push(... {shallow:true}) on first page load gets cancelled.

Unanswered
Wuchang bream posted this in #help-forum
Open in Discord
Wuchang breamOP
Having some trouble debugging an issue (with the pages router):

When my component first loads, I want to replaceState if certain conditions are met (if a parameter isn't present, I update the url with a default value). To do this, my useEffect hook looks something like:

const isReady = useRouter.isReady()
useEffect(() => {
  if(!isReady) return
  if(someCondition) {
    Router.push({query: "?param=foo"}, undefined, {shallow: true})
  }
}, [isReady, someCondition])


When navigating to this page via an in-up link, this seems to work fine. However, on first page load -- it doesn't do anything at all!

By looking at the promise that Router.push returns, I was able to see that it resolves to false (a doc update might be useful to explain the possible return values of Router.push/replace; and when/why navigation is expected to be cancelled). Debugging through Next.js code seems to show that lastRenderReject is what is getting in the way of the router update. For context, here is the relevant library code ( https://github.com/vercel/next.js/blob/9fb18e673998635457903663205d10f6cc9b3849/packages/next/src/client/index.tsx#L654):

  if (lastRenderReject) {
      lastRenderReject();
  }
  resolvePromise = ()=>{
      lastRenderReject = null;
      resolve();
  }
  ;
  lastRenderReject = ()=>{
      canceled = true;
      lastRenderReject = null;
      const error = new Error("Cancel rendering route");
      error.cancelled = true;
      reject(error);
  }


Ultimately, it seems like.. I'm too early in the rendering cycle -- and the page is still navigating (even though router isReady)? ...or something? If I add something like:
  const [isHydrated, setIsHydrated] = useState(false)

  // Wait till Next.js rehydration completes
  useEffect(() => {
    setIsHydrated(true)
  }, [])


And wait for isHydrated; that seems to be an effective workaround -- but I don't want to introduce it without first understanding why it's necessary.

5 Replies

Wuchang breamOP
Wuchang breamOP
uhoh. I think this was just React's strict mode. The first render succeeds and then gets overriden by another render that believes it's still inital. I am updating a variable in my useEffect to prevent redirecting twice, and this is coming back to bite me.
Something else is happening :/
Wuchang breamOP
ComponentDidMount in next.js is firing after my component's useEffect. Still not sure why. This seems to be causing a re-render of initial state and a hard navigation.
Wuchang breamOP
I think I got it. Next.js ComponentDidMount is being called twice in react dev strict mode. ComponentDidMount has a side-effect that updates the router state + window location state (this is unexpected, at least by me). Because my useEffect also tries to update window location state, there is a conflict and it gets overridden.

My new workaround:

const isReady = useRouter.isReady()
const pageInitialized = useRef(false)
useEffect(() => {
    if (pageInitialized.current) return // already initialized.
    if(!isReady) return
    if(someCondition) {
      Router.push({query: "?param=foo"}, undefined, {shallow: true}).then((val) => {
        if (!val) pageInitialized.current = false // <---- this is the new code!
      })
      pageInitialized.current = true
  }
}    
Wuchang breamOP
The above workaround code has a subtle bug. For at least one render, the component will consider itself initialized even though it will be reverted. A better fix is:

const isReady = useRouter.isReady()
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
  if (!isReady || isInitialized) return
  if (someCondition) {
    Router.push({query: "?param=foo"}, undefined, {shallow: true}).then((success) => {
      if (success) {
        setIsInitialized(true)
      } else {
        Router.push({query: "?param=foo"}, undefined, {shallow: true}).then((success) => {
          // second time is the charm...? :/
          setIsInitialized(true)
        }
      }
    })
  }
}