Skip to content

Commit

Permalink
fix: Race condition with navigation
Browse files Browse the repository at this point in the history
Use `useInsertionEffect` to subscribe to navigation events
rather than `useEffect`, which can happen too late.

Add demo that cycles between various types of components to test.

See #343 (comment)
  • Loading branch information
franky47 committed Sep 12, 2023
1 parent 59bc915 commit be7b5bb
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/app/app/routing-tour/_components/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { parseAsInteger, parseAsString } from '../../../../../dist/parsers'

export const counterParser = parseAsInteger.withDefault(0)
export const fromParser = parseAsString
33 changes: 33 additions & 0 deletions src/app/app/routing-tour/_components/view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import Link from 'next/link'
import { useQueryState } from '../../../../../dist'
import { counterParser, fromParser } from './parsers'

type RoutingTourViewProps = {
thisPage: string
nextPage: string
}

export const RoutingTourView: React.FC<RoutingTourViewProps> = ({
thisPage,
nextPage
}) => {
const [counter] = useQueryState('counter', counterParser)
const [from] = useQueryState('from', fromParser)
return (
<>
<Link
href={`/app/routing-tour/${nextPage}?from=${thisPage}&counter=${
counter + 1
}`}
>
Next
</Link>
<p>Came from: {from}</p>
<p>This page: {thisPage}</p>
<p>Next page: {nextPage}</p>
<p>Counter: {counter}</p>
</>
)
}
10 changes: 10 additions & 0 deletions src/app/app/routing-tour/a/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Suspense } from 'react'
import { RoutingTourView } from '../_components/view'

export default function PageA() {
return (
<Suspense>
<RoutingTourView thisPage="a" nextPage="b" />
</Suspense>
)
}
10 changes: 10 additions & 0 deletions src/app/app/routing-tour/b/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Suspense } from 'react'
import { RoutingTourView } from '../_components/view'

export default function PageB() {
return (
<Suspense>
<RoutingTourView thisPage="b" nextPage="c" />
</Suspense>
)
}
7 changes: 7 additions & 0 deletions src/app/app/routing-tour/c/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import { RoutingTourView } from '../_components/view'

export default function PageC() {
return <RoutingTourView thisPage="c" nextPage="d" />
}
7 changes: 7 additions & 0 deletions src/app/app/routing-tour/d/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import { RoutingTourView } from '../_components/view'

export default function PageD() {
return <RoutingTourView thisPage="d" nextPage="a" />
}
22 changes: 22 additions & 0 deletions src/app/app/routing-tour/start/client/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import Link from 'next/link'

export default function ServerStartPage() {
return (
<ul>
<li>
<Link href="/app/routing-tour/a?from=start.client">a (server)</Link>
</li>
<li>
<Link href="/app/routing-tour/b?from=start.client">b (server)</Link>
</li>
<li>
<Link href="/app/routing-tour/c?from=start.client">c (client)</Link>
</li>
<li>
<Link href="/app/routing-tour/d?from=start.client">d (client)</Link>
</li>
</ul>
)
}
20 changes: 20 additions & 0 deletions src/app/app/routing-tour/start/server/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link'

export default function ServerStartPage() {
return (
<ul>
<li>
<Link href="/app/routing-tour/a?from=start.client">a (server)</Link>
</li>
<li>
<Link href="/app/routing-tour/b?from=start.client">b (server)</Link>
</li>
<li>
<Link href="/app/routing-tour/c?from=start.client">c (client)</Link>
</li>
<li>
<Link href="/app/routing-tour/d?from=start.client">d (client)</Link>
</li>
</ul>
)
}
3 changes: 3 additions & 0 deletions src/lib/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,20 @@ export function usePatchedHistory() {
if (patched) {
return
}
// console.debug('Patching history')
for (const method of ['pushState', 'replaceState'] as const) {
const original = window.history[method].bind(window.history)
window.history[method] = function nextUseQueryState_patchedHistory(
state: any,
title: string,
url?: string | URL | null
) {
// console.debug(`history.${method}(${url}) ${title} %O`, state)
// If someone else than our hooks have updated the URL,
// send out a signal for them to sync their internal state.
if (title !== NOSYNC_MARKER && url) {
const search = new URL(url, location.origin).searchParams
// console.debug(`Triggering sync with ${search.toString()}`)
// Here we're delaying application to next tick to avoid:
// `Warning: useInsertionEffect must not schedule updates.`
//
Expand Down
4 changes: 3 additions & 1 deletion src/lib/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,18 @@ export function useQueryState<T = string>(
// console.debug(`render ${key}: ${internalState}`)

// Sync all hooks together & with external URL changes
React.useEffect(() => {
React.useInsertionEffect(() => {
function syncFromURL(search: URLSearchParams) {
const value = search.get(key) ?? null
const v = value === null ? null : parse(value)
// console.debug(`sync ${key}: ${v}`)
setInternalState(v)
}
// console.debug(`Subscribing to sync for \`${key}\``)
emitter.on(key, setInternalState)
emitter.on(SYNC_EVENT_KEY, syncFromURL)
return () => {
// console.debug(`Unsubscribing from sync for \`${key}\``)
emitter.off(key, setInternalState)
emitter.off(SYNC_EVENT_KEY, syncFromURL)
}
Expand Down

0 comments on commit be7b5bb

Please sign in to comment.