Skip to content

Commit

Permalink
Merge pull request #26874 from storybookjs/yann/fix-nextjs-14.2-usepa…
Browse files Browse the repository at this point in the history
…rams

Nextjs: Support next 14.2 useParams functionality
(cherry picked from commit 316779b)
  • Loading branch information
yannbf committed Apr 19, 2024
1 parent afc0a44 commit 7400b91
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 175 deletions.
128 changes: 81 additions & 47 deletions code/frameworks/nextjs/src/routing/app-router-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import {
LayoutRouterContext,
AppRouterContext,
Expand All @@ -7,7 +7,10 @@ import {
import {
PathnameContext,
SearchParamsContext,
PathParamsContext,
} from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
import { type Params } from 'next/dist/shared/lib/router/utils/route-matcher';
import { PAGE_SEGMENT_KEY } from 'next/dist/shared/lib/segment';
import type { FlightRouterState } from 'next/dist/server/app-render/types';
import type { RouteParams } from './types';

Expand All @@ -16,6 +19,32 @@ type AppRouterProviderProps = {
routeParams: RouteParams;
};

// Since Next 14.2.x
// https://github.com/vercel/next.js/pull/60708/files#diff-7b6239af735eba0c401e1a0db1a04dd4575c19a031934f02d128cf3ac813757bR106
function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): Params {
const parallelRoutes = currentTree[1];

for (const parallelRoute of Object.values(parallelRoutes)) {
const segment = parallelRoute[0];
const isDynamicParameter = Array.isArray(segment);
const segmentValue = isDynamicParameter ? segment[1] : segment;
if (!segmentValue || segmentValue.startsWith(PAGE_SEGMENT_KEY)) continue;

// Ensure catchAll and optional catchall are turned into an array
const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc');

if (isCatchAll) {
params[segment[0]] = segment[1].split('/');
} else if (isDynamicParameter) {
params[segment[0]] = segment[1];
}

params = getSelectedParams(parallelRoute, params);
}

return params;
}

const getParallelRoutes = (segmentsList: Array<string>): FlightRouterState => {
const segment = segmentsList.shift();

Expand All @@ -34,62 +63,67 @@ export const AppRouterProvider: React.FC<React.PropsWithChildren<AppRouterProvid
const { pathname, query, segments = [], ...restRouteParams } = routeParams;

const tree: FlightRouterState = [pathname, { children: getParallelRoutes([...segments]) }];
const pathParams = useMemo(() => {
return getSelectedParams(tree);
}, [tree]);

// https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L436
return (
<PathnameContext.Provider value={pathname}>
<SearchParamsContext.Provider value={new URLSearchParams(query)}>
<GlobalLayoutRouterContext.Provider
value={{
changeByServerResponse() {
// NOOP
},
buildId: 'storybook',
tree,
focusAndScrollRef: {
apply: false,
hashFragment: null,
segmentPaths: [tree],
onlyHashChange: false,
},
nextUrl: pathname,
}}
>
<AppRouterContext.Provider
<PathParamsContext.Provider value={pathParams}>
<PathnameContext.Provider value={pathname}>
<SearchParamsContext.Provider value={new URLSearchParams(query)}>
<GlobalLayoutRouterContext.Provider
value={{
push(...args) {
action('nextNavigation.push')(...args);
},
replace(...args) {
action('nextNavigation.replace')(...args);
},
forward(...args) {
action('nextNavigation.forward')(...args);
},
back(...args) {
action('nextNavigation.back')(...args);
},
prefetch(...args) {
action('nextNavigation.prefetch')(...args);
changeByServerResponse() {
// NOOP
},
refresh: () => {
action('nextNavigation.refresh')();
buildId: 'storybook',
tree,
focusAndScrollRef: {
apply: false,
hashFragment: null,
segmentPaths: [tree],
onlyHashChange: false,
},
...restRouteParams,
nextUrl: pathname,
}}
>
<LayoutRouterContext.Provider
<AppRouterContext.Provider
value={{
childNodes: new Map(),
tree,
url: pathname,
push(...args) {
action('nextNavigation.push')(...args);
},
replace(...args) {
action('nextNavigation.replace')(...args);
},
forward(...args) {
action('nextNavigation.forward')(...args);
},
back(...args) {
action('nextNavigation.back')(...args);
},
prefetch(...args) {
action('nextNavigation.prefetch')(...args);
},
refresh: () => {
action('nextNavigation.refresh')();
},
...restRouteParams,
}}
>
{children}
</LayoutRouterContext.Provider>
</AppRouterContext.Provider>
</GlobalLayoutRouterContext.Provider>
</SearchParamsContext.Provider>
</PathnameContext.Provider>
<LayoutRouterContext.Provider
value={{
childNodes: new Map(),
tree,
url: pathname,
}}
>
{children}
</LayoutRouterContext.Provider>
</AppRouterContext.Provider>
</GlobalLayoutRouterContext.Provider>
</SearchParamsContext.Provider>
</PathnameContext.Provider>
</PathParamsContext.Provider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
};

export const Default = {
play: async ({ canvasElement }) => {
play: async () => {
await waitFor(() => expect(document.title).toEqual('Next.js Head Title'));
await expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1);
await expect(document.querySelector('meta[property="og:title"]').content).toEqual(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// usePathname and useSearchParams are only usable if experimental: {appDir: true} is set in next.config.js
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import {
useRouter,
usePathname,
useSearchParams,
useParams,
useSelectedLayoutSegment,
useSelectedLayoutSegments,
} from 'next/navigation';
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';

function Component() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const params = useParams();
const segment = useSelectedLayoutSegment();
const segments = useSelectedLayoutSegments();

const searchParamsList = searchParams ? Array.from(searchParams.entries()) : [];

Expand Down Expand Up @@ -42,6 +51,8 @@ function Component() {
return (
<div>
<div>pathname: {pathname}</div>
<div>segment: {segment}</div>
<div>segments: {segments.join(',')}</div>
<div>
searchparams:{' '}
<ul>
Expand All @@ -52,6 +63,16 @@ function Component() {
))}
</ul>
</div>
<div>
params:{' '}
<ul>
{Object.entries(params).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
{routerActions.map(({ cb, name }) => (
<div key={name} style={{ marginBottom: '1em' }}>
<button type="button" onClick={cb}>
Expand All @@ -63,6 +84,8 @@ function Component() {
);
}

type Story = StoryObj<typeof Component>;

export default {
component: Component,
parameters: {
Expand All @@ -78,4 +101,29 @@ export default {
},
} as Meta<typeof Component>;

export const Default: StoryObj<typeof Component> = {};
export const Default: Story = {};

export const WithSegmentDefined: Story = {
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: ['dashboard', 'settings'],
},
},
},
};

export const WithSegmentDefinedForParams: Story = {
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: [
['slug', 'hello'],
['framework', 'nextjs'],
],
},
},
},
};

0 comments on commit 7400b91

Please sign in to comment.