Skip to content

Commit

Permalink
fix: Output a debug build with user timings marks
Browse files Browse the repository at this point in the history
Not really a fix, but this triggers a patch update under SemRel.
  • Loading branch information
franky47 committed Oct 22, 2023
1 parent afd3459 commit 989f165
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 35 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,24 @@ React.useEffect(

See issue #259 for more testing-related discussions.

## Debugging

You can enable debug logs by importing from `next-usequerystate/debug`:

```ts
import { useQueryState } from 'next-usequerystate/debug'

// API is unchanged.
// Parsers can still be imported server-side from `next-usequerystate/parsers`
// as they don't print out logs.
```

Log lines will be prefixed with `[nuqs]` for `useQueryState` and `[nuq+]` for
`useQueryStates`.

User timings markers are also recorded, for advanced performance analysis using
your browser's devtools.

## Caveats

Because the Next.js **pages router** is not available in an SSR context, this
Expand Down
7 changes: 7 additions & 0 deletions packages/next-usequerystate/debug.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is needed for projects that have `moduleResolution` set to `node`
// in their tsconfig.json to be able to `import {} from 'next-usequerystate/debug'`.
// Other module resolutions strategies will look for the `exports` in `package.json`,
// but with `node`, TypeScript will look for a .d.ts file with that name at the
// root of the package.

export * from './dist/debug'
9 changes: 8 additions & 1 deletion packages/next-usequerystate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@
"types": "./dist/parsers.d.ts",
"import": "./dist/parsers.js",
"require": "./dist/parsers.cjs"
},
"./debug": {
"types": "./dist/debug/index.d.ts",
"import": "./dist/debug/index.js",
"require": "./dist/debug/index.cjs"
}
},
"scripts": {
"dev": "tsup --format esm --watch --external=react",
"build": "NODE_ENV=production tsup --clean --format esm,cjs --external=react",
"build": "run-p build:*",
"build:prod": "NODE_ENV=production tsup --clean --external=react",
"build:debug": "NODE_ENV=development tsup --clean --external=react",
"test": "run-p test:*",
"test:types": "tsd",
"test:unit": "vitest run",
Expand Down
5 changes: 5 additions & 0 deletions packages/next-usequerystate/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ if (!patched && typeof window === 'object') {
// Null URL is only used for state changes,
// we're not interested in reacting to those.
__DEBUG__ &&
performance.mark(`[nuqs] history.${method}(null) (${title})`) &&
console.debug(`[nuqs] history.${method}(null) (${title}) %O`, state)
return original(state, title, url)
}
const source = title === NOSYNC_MARKER ? 'internal' : 'external'
const search = new URL(url, location.origin).searchParams
__DEBUG__ &&
performance.mark(`[nuqs] history.${method}(${url}) (${source})`) &&
console.debug(`[nuqs] history.${method}(${url}) (${source}) %O`, state)
// If someone else than our hooks have updated the URL,
// send out a signal for them to sync their internal state.
Expand All @@ -62,6 +64,9 @@ if (!patched && typeof window === 'object') {
// to rely on the URL being up to date.
setTimeout(() => {
__DEBUG__ &&
performance.mark(
`[nuqs] External history.${method} call: triggering sync with ${search.toString()}`
) &&
console.debug(
`[nuqs] External history.${method} call: triggering sync with ${search.toString()}`
)
Expand Down
22 changes: 18 additions & 4 deletions packages/next-usequerystate/src/update-queue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Options, Router } from './defs'
import type { Options, Router } from './defs'
import { NOSYNC_MARKER } from './sync'

// 50ms between calls to the history API seems to satisfy Chrome and Firefox.
Expand All @@ -22,7 +22,16 @@ export function enqueueQueryStringUpdate<Value>(
serialize: (value: Value) => string,
options: Options
) {
updateQueue.set(key, value === null ? null : serialize(value))
const serializedOrNull = value === null ? null : serialize(value)
__DEBUG__ &&
performance.mark(`[nuqs queue] Enqueueing ${key}=${serializedOrNull}`) &&
console.debug(
'[nuqs queue] Enqueueing %s=%s %O',
key,
serializedOrNull,
options
)
updateQueue.set(key, serializedOrNull)
// Any item can override an option for the whole batch of updates
if (options.history === 'push') {
queueOptions.history = 'push'
Expand Down Expand Up @@ -59,6 +68,7 @@ export function flushToURL(router: Router) {
Math.min(FLUSH_RATE_LIMIT_MS, FLUSH_RATE_LIMIT_MS - timeSinceLastFlush)
)
__DEBUG__ &&
performance.mark(`[nuqs queue] Scheduling flush in ${flushInMs} ms`) &&
console.debug('[nuqs queue] Scheduling flush in %f ms', flushInMs)
setTimeout(() => {
lastFlushTimestamp = performance.now()
Expand Down Expand Up @@ -88,7 +98,9 @@ function flushUpdateQueue(router: Router) {
queueOptions.scroll = false
queueOptions.shallow = true
updateQueue.clear()
__DEBUG__ && console.debug('[nuqs queue] Flushing queue %O', items)
__DEBUG__ &&
performance.mark('[nuqs queue] Flushing queue') &&
console.debug('[nuqs queue] Flushing queue %O', items)

for (const [key, value] of items) {
if (value === null) {
Expand All @@ -106,7 +118,9 @@ function flushUpdateQueue(router: Router) {
// otherwise using a relative URL works just fine.
// todo: Does it when using the router with `shallow: false` on dynamic paths?
const url = query ? `?${query}${hash}` : `${path}${hash}`
__DEBUG__ && console.debug('[nuqs queue] Updating url: %s', url)
__DEBUG__ &&
performance.mark(`[nuqs queue] Updating url: ${url}`) &&
console.debug('[nuqs queue] Updating url: %s', url)
try {
if (options.shallow) {
const updateUrl =
Expand Down
30 changes: 18 additions & 12 deletions packages/next-usequerystate/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,6 @@ export function useQueryState<T = string>(
const router = useRouter()
// Not reactive, but available on the server and on page load
const initialSearchParams = useSearchParams()
__DEBUG__ &&
console.debug(
`[nuqs \`${key}\`] initialSearchParams: ${
initialSearchParams === null ? 'null' : initialSearchParams.toString()
}`
)
const [internalState, setInternalState] = React.useState<T | null>(() => {
const queueValue = getInitialStateFromQueue(key)
const urlValue =
Expand All @@ -231,28 +225,42 @@ export function useQueryState<T = string>(
return value === null ? null : parse(value)
})
const stateRef = React.useRef(internalState)
__DEBUG__ && console.debug(`[nuqs \`${key}\`] render: ${internalState}`)
__DEBUG__ &&
performance.mark(`[nuqs \`${key}\`] render - state: ${internalState}`) &&
console.debug(
`[nuqs \`${key}\`] render - state: \`%O\` - initialSearchParams: \`${
initialSearchParams === null ? 'null' : initialSearchParams.toString()
}\``,
internalState
)

// Sync all hooks together & with external URL changes
React.useInsertionEffect(() => {
function updateInternalState(state: T | null) {
__DEBUG__ &&
performance.mark(`[nuqs \`${key}\`] updateInternalState ${state}`) &&
console.debug(`[nuqs \`${key}\`] updateInternalState %O`, state)
stateRef.current = state
setInternalState(state)
}
function syncFromURL(search: URLSearchParams) {
const value = search.get(key) ?? null
const state = value === null ? null : parse(value)
__DEBUG__ && console.debug(`[nuqs \`${key}\`] syncFromURL: %O`, state)
__DEBUG__ &&
performance.mark(`[nuqs \`${key}\`] syncFromURL: ${state}`) &&
console.debug(`[nuqs \`${key}\`] syncFromURL: %O`, state)
updateInternalState(state)
}
__DEBUG__ && console.debug(`[nuqs \`${key}\`] Subscribing to sync`)
__DEBUG__ &&
performance.mark(`[nuqs \`${key}\`] Subscribing to sync`) &&
console.debug(`[nuqs \`${key}\`] Subscribing to sync`)

emitter.on(SYNC_EVENT_KEY, syncFromURL)
emitter.on(key, updateInternalState)
return () => {
__DEBUG__ && console.debug(`[nuqs \`${key}\`] Unsubscribing from sync`)
__DEBUG__ &&
performance.mark(`[nuqs \`${key}\`] Unsubscribing from sync`) &&
console.debug(`[nuqs \`${key}\`] Unsubscribing from sync`)
emitter.off(SYNC_EVENT_KEY, syncFromURL)
emitter.off(key, updateInternalState)
}
Expand All @@ -264,8 +272,6 @@ export function useQueryState<T = string>(
? stateUpdater(stateRef.current ?? defaultValue ?? null)
: stateUpdater

__DEBUG__ &&
console.debug(`[nuqs \`${key}\`] Emitting and queueing: %O`, newValue)
// Sync all hooks state (including this one)
emitter.emit(key, newValue)
enqueueQueryStringUpdate(key, newValue, serialize, {
Expand Down
57 changes: 42 additions & 15 deletions packages/next-usequerystate/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
const router = useRouter()
// Not reactive, but available on the server and on page load
const initialSearchParams = useSearchParams()
__DEBUG__ &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] initialSearchParams: ${
initialSearchParams === null ? 'null' : initialSearchParams.toString()
}`
)
const [internalState, setInternalState] = React.useState<V>(() => {
if (typeof window !== 'object') {
// SSR
Expand All @@ -81,15 +75,28 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
})
const stateRef = React.useRef(internalState)
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] render ${JSON.stringify(
internalState
)}`
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] render : %O`,
internalState
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] render - state: %O, initialSearchParams: %O`,
internalState,
initialSearchParams
)

// Sync all hooks together & with external URL changes
React.useInsertionEffect(() => {
function updateInternalState(state: V) {
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] updateInternalState ${JSON.stringify(state)}`
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] updateInternalState %O`,
state
Expand All @@ -100,6 +107,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
function syncFromURL(search: URLSearchParams) {
const state = parseMap(keyMap, search)
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] syncFromURL: ${JSON.stringify(state)}`
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] syncFromURL: %O`,
state
Expand All @@ -116,6 +128,13 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
[key as keyof V]: value ?? defaultValue ?? null
}
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] Cross-hook key sync \`${key}\`: ${value} (default: ${defaultValue}). Resolved state: ${JSON.stringify(
stateRef.current
)}`
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(
','
Expand All @@ -132,6 +151,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
emitter.on(SYNC_EVENT_KEY, syncFromURL)
for (const key of Object.keys(keyMap)) {
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] Subscribing to sync for \`${key}\``
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(
','
Expand All @@ -143,6 +167,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
emitter.off(SYNC_EVENT_KEY, syncFromURL)
for (const key of Object.keys(keyMap)) {
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] Unsubscribing to sync for \`${key}\``
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(
','
Expand All @@ -160,6 +189,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
? stateUpdater(stateRef.current)
: stateUpdater
__DEBUG__ &&
performance.mark(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] setState: ${JSON.stringify(newState)}`
) &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(',')}\`] setState: %O`,
newState
Expand All @@ -170,13 +204,6 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
continue
}
emitter.emit(key, value)
__DEBUG__ &&
console.debug(
`[nuq+ \`${Object.keys(keyMap).join(
','
)}\`] Emitting and queueing: \`${key}\`: %O`,
value
)
enqueueQueryStringUpdate(key, value, config.serialize ?? String, {
// Call-level options take precedence over hook declaration options.
history: options.history ?? history,
Expand Down
12 changes: 9 additions & 3 deletions packages/next-usequerystate/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { defineConfig } from 'tsup'

const isDebugBuild = process.env.NODE_ENV !== 'production'

export default defineConfig({
entry: ['src/index.ts', 'src/parsers.ts'],
splitting: true,
entry: isDebugBuild ? ['src/index.ts'] : ['src/index.ts', 'src/parsers.ts'],
format: ['esm', 'cjs'],
dts: true,
outDir: isDebugBuild ? 'dist/debug' : 'dist',
splitting: true,
treeshake: true,
sourcemap: isDebugBuild ? 'inline' : false,
define: {
__DEBUG__: process.env.NODE_ENV === 'production' ? 'false' : 'true'
__DEBUG__: isDebugBuild ? 'true' : 'false'
}
})

0 comments on commit 989f165

Please sign in to comment.