Skip to content

Commit

Permalink
feat(next-app-router): listen to external URL changes (#6107)
Browse files Browse the repository at this point in the history
  • Loading branch information
aymeric-giraudet committed Mar 27, 2024
1 parent 1778b31 commit 6f1f40b
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 49 deletions.
52 changes: 3 additions & 49 deletions packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import historyRouter from 'instantsearch.js/es/lib/routers/history';
import { safelyRunOnBrowser } from 'instantsearch.js/es/lib/utils';
import { headers } from 'next/headers';
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
import React, { useEffect, useRef } from 'react';
import {
InstantSearch,
Expand All @@ -11,6 +8,7 @@ import {

import { InitializePromise } from './InitializePromise';
import { TriggerSearch } from './TriggerSearch';
import { useInstantSearchRouting } from './useInstantSearchRouting';
import { warn } from './warn';

import type { InitialResults, StateMapping, UiState } from 'instantsearch.js';
Expand Down Expand Up @@ -47,10 +45,6 @@ export function InstantSearchNext<
routing: passedRouting,
...instantSearchProps
}: InstantSearchNextProps<TUiState, TRouteState>) {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const isMounting = useRef(true);
useEffect(() => {
isMounting.current = false;
Expand All @@ -60,54 +54,14 @@ export function InstantSearchNext<
};
}, []);

const routing = useInstantSearchRouting(passedRouting, isMounting);

const promiseRef = useRef<PromiseWithState<void> | null>(null);

const initialResults = safelyRunOnBrowser(
() => window[InstantSearchInitialResults]
);

const routing: InstantSearchProps<TUiState, TRouteState>['routing'] =
passedRouting && {};
if (routing) {
let browserHistoryOptions: Partial<BrowserHistoryArgs<TRouteState>> = {};

browserHistoryOptions.getLocation = () => {
if (typeof window === 'undefined') {
const url = `${
headers().get('x-forwarded-proto') || 'http'
}://${headers().get('host')}${pathname}?${searchParams}`;
return new URL(url) as unknown as Location;
}

if (isMounting.current) {
return new URL(
`${window.location.protocol}//${window.location.host}${pathname}?${searchParams}`
) as unknown as Location;
}

return window.location;
};
browserHistoryOptions.push = function push(
this: ReturnType<typeof historyRouter>,
url
) {
// This is to skip the push with empty routeState on dispose as it would clear params set on a <Link>
if (this.isDisposed) {
return;
}
router.push(url, { scroll: false });
};

if (typeof passedRouting === 'object') {
browserHistoryOptions = {
...browserHistoryOptions,
...passedRouting.router,
};
routing.stateMapping = passedRouting.stateMapping;
}
routing.router = historyRouter(browserHistoryOptions);
}

warn(
false,
`InstantSearchNext relies on experimental APIs and may break in the future.
Expand Down
75 changes: 75 additions & 0 deletions packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import historyRouter from 'instantsearch.js/es/lib/routers/history';
import { headers } from 'next/headers';
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
import { useRef, useEffect } from 'react';

import type { InstantSearchNextProps } from './InstantSearchNext';
import type { UiState } from 'instantsearch.js';
import type { BrowserHistoryArgs } from 'instantsearch.js/es/lib/routers/history';
import type { InstantSearchProps } from 'react-instantsearch-core';

export function useInstantSearchRouting<
TUiState extends UiState = UiState,
TRouteState = TUiState
>(
passedRouting: InstantSearchNextProps<TUiState, TRouteState>['routing'],
isMounting: React.MutableRefObject<boolean>
) {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const routingRef =
useRef<InstantSearchProps<TUiState, TRouteState>['routing']>();
const onUpdateRef = useRef<() => void>();
useEffect(() => {
if (onUpdateRef.current) {
onUpdateRef.current();
}
}, [pathname, searchParams]);

if (passedRouting && !routingRef.current) {
let browserHistoryOptions: Partial<BrowserHistoryArgs<TRouteState>> = {};

browserHistoryOptions.getLocation = () => {
if (typeof window === 'undefined') {
const url = `${
headers().get('x-forwarded-proto') || 'http'
}://${headers().get('host')}${pathname}?${searchParams}`;
return new URL(url) as unknown as Location;
}

if (isMounting.current) {
return new URL(
`${window.location.protocol}//${window.location.host}${pathname}?${searchParams}`
) as unknown as Location;
}

return window.location;
};
browserHistoryOptions.push = function push(
this: ReturnType<typeof historyRouter>,
url
) {
// This is to skip the push with empty routeState on dispose as it would clear params set on a <Link>
if (this.isDisposed) {
return;
}
router.push(url, { scroll: false });
};
browserHistoryOptions.start = function start(onUpdate) {
onUpdateRef.current = onUpdate;
};

routingRef.current = {};
if (typeof passedRouting === 'object') {
browserHistoryOptions = {
...browserHistoryOptions,
...passedRouting.router,
};
routingRef.current.stateMapping = passedRouting.stateMapping;
}
routingRef.current.router = historyRouter(browserHistoryOptions);
}

return routingRef.current;
}

0 comments on commit 6f1f40b

Please sign in to comment.