vM.

How to Pass Search Params and Page Data to Layouts in the Next.js App Router

Author
Vishal Maurya
Published on
Reading time
5 min read

Overview

You want a layout, such as a sidebar or header, to react to a query string or to data loaded by a page. You look for searchParams in the layout props and find nothing, or the layout keeps showing old values after navigation. This is by design, and the fix is to choose the right pattern for what you need.

1. Why Layouts Behave This Way

Layouts are preserved across navigations between pages that share them. They do not re-render when you move between child routes, which keeps state and avoids repeated work. Because of that, a layout receives params for its own segment but not searchParams, and it cannot read data produced by a child page.

Also, data does not flow upward in React. A page cannot pass props to its parent layout. Design around that rather than fighting it.

2. Read searchParams in the Page

If the content depends on the query string, render it in the page, which receives searchParams:

// app/products/page.tsx
type Props = { searchParams: Promise<{ q?: string; sort?: string }> };

export default async function ProductsPage({ searchParams }: Props) {
  const { q = '', sort = 'name' } = await searchParams;
  // query using q and sort
  return <h1>Results for {q}</h1>;
}

In Next.js 14, searchParams is a plain object, not a Promise. Using it also makes the route dynamic.

If a UI element in the layout must change per query, move that element into the page or a template-level component, or use the client hook below.

3. Use Client Hooks for Layout UI

A client component inside a layout can read the current URL with useSearchParams and usePathname, which update on navigation:

// components/active-filter.tsx
'use client';

import { useSearchParams, usePathname } from 'next/navigation';

export function ActiveFilter() {
  const params = useSearchParams();
  const pathname = usePathname();
  const category = params.get('category') ?? 'all';
  return <p data-path={pathname}>Filter: {category}</p>;
}
// app/products/layout.tsx
import { Suspense } from 'react';
import { ActiveFilter } from '@/components/active-filter';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <Suspense fallback={null}>
        <ActiveFilter />
      </Suspense>
      {children}
    </div>
  );
}

Wrapping in Suspense matters: in statically rendered routes, useSearchParams without a boundary can cause build errors or push the whole page to client rendering. Check your version's behavior.

4. Updating Search Params From the Layout

To change filters from a layout control, build a new query string and navigate:

'use client';

import { useRouter, usePathname, useSearchParams } from 'next/navigation';

export function SortSelect() {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();

  function onChange(value: string) {
    const next = new URLSearchParams(params.toString());
    next.set('sort', value);
    router.push(`${pathname}?${next.toString()}`);
  }

  return (
    <select defaultValue={params.get('sort') ?? 'name'} onChange={(e) => onChange(e.target.value)}>
      <option value="name">Name</option>
      <option value="price">Price</option>
    </select>
  );
}

The page re-renders with new searchParams, while the layout stays mounted.

5. Sharing Page Data With a Layout

Options, from simplest to most flexible:

  • Fetch twice with caching: both layout and page call the same query function. Wrap it with React cache so a single render pass deduplicates calls to the same function with the same arguments.
  • Lift to the layout: if both need the data, fetch it in the layout and pass it down through props or context to client children. Only works for data that does not depend on a child segment.
  • Render in the page: if the UI depends on page data, render it in the page instead of the layout.
// lib/queries.ts
import { cache } from 'react';

export const getCategory = cache(async (slug: string) => {
  // return await db.category.findUnique({ where: { slug } });
  return { slug, name: slug };
});

Remember that layouts and pages render in parallel and layouts do not re-run on every navigation, so avoid relying on a layout for freshness-sensitive checks.

6. Parallel Routes for Independent Regions

If a region of the layout needs its own data driven by the URL, a parallel route slot can render it. Slots are folders prefixed with @ and are passed to the layout as props. Each slot is a page-like segment, so it receives searchParams and can load its own data:

// app/dashboard/layout.tsx
export default function Layout({
  children,
  stats,
}: {
  children: React.ReactNode;
  stats: React.ReactNode;
}) {
  return (
    <div>
      <aside>{stats}</aside>
      <main>{children}</main>
    </div>
  );
}

Here app/dashboard/@stats/page.tsx defines the slot. Parallel routes add complexity, including default files for unmatched slots, so use them when a simpler option does not fit.

Conclusion

Do not expect layouts to receive query strings or page data. Render URL-dependent content in pages, use client hooks for small layout widgets, share data through cached functions, and use parallel routes when a region needs independent loading.

If your layout hierarchy has become tangled, I can help restructure it. Contact me.

Additional Resources

  • Layouts and pages
  • useSearchParams reference
  • Parallel routes