Skip to content

Empty root pages/ folder (App Router guide) silently changes next/navigation hook signatures to the Pages-Router | null compat types #926

Description

@y-hsgw

Summary

The App Router guide recommends, for the app-layer conflict:

The solution is to move the Next.js app folder to the project root and import FSD pages from src ... into the Next.js app folder.
You will also need to add a pages folder to the project root, otherwise Next.js will try to use src/pages as the Pages Router even if you use the App Router, which will break the build.

This works for building, but the empty root pages/ folder has a non-obvious side effect: it changes the type signatures of the next/navigation hooks across the whole app, even though the project only uses the App Router.

This is not just "a type error in my code" — the public API signature itself changes. I think the guide should at least document this trade-off.

Why it happens

When a project contains both an app and a pages directory, Next.js switches the next/navigation hooks to their Pages-Router compatibility signatures, which add | null. This is what the bundled type augmentation in next does — next/navigation-types/compat/navigation.d.ts (Next.js 16.2.6):

declare module 'next/navigation' {
  export function useSearchParams(): ReadonlyURLSearchParams | null
  export function usePathname(): string | null
  export function useParams<
    T extends Record<string, string | string[]> = Record<string, string | string[]>,
  >(): T | null
  export function useSelectedLayoutSegments(): string[] | null
  export function useSelectedLayoutSegment(): string | null
}

So following the FSD guide (adding an empty pages/ purely to satisfy the Pages-Router probe) opts an App-Router-only project into Pages-Router migration-compat types it does not want — for all five hooks above.

The official docs describe this for useSearchParams:

If an application includes the /pages directory, useSearchParams will return ReadonlyURLSearchParams | null. The null value is for compatibility during migration since search params cannot be known during prerendering of a page that doesn't use getServerSideProps

https://nextjs.org/docs/app/api-reference/functions/use-search-params#returns

For reference, usePathname documents the same automatic adjustment ("if your project contains both an app and a pages directory, Next.js will automatically adjust the return type of usePathname"):

Impact

  • useSearchParams, usePathname, useParams, useSelectedLayoutSegments, useSelectedLayoutSegment all become nullable.
  • Under strict TS, existing call sites like useSearchParams().toString() or .get(...) now error (TS18047: ... is possibly 'null'), forcing either null guards or non-null assertions throughout the codebase — purely as a consequence of a directory that exists only to silence a build probe.
  • This becomes a barrier specifically for developers who already use the App Router and want to adopt FSD: their project has no pages/ directory today, so the next/navigation hooks are non-nullable. The moment they follow this guide and add the empty pages/ folder, every existing call site is pushed onto the | null compat signatures and starts failing type-check — purely as a consequence of adopting FSD, not because of any real Pages-Router usage.

Suggestion

At minimum, the App Router guide should document this signature side effect so it is not a surprise.

It would also help to discuss alternatives for App-Router-only projects that want to avoid the compat nullability:

  1. Rename just the FSD pages layer (e.g. to views), so no pages/ directory exists at all.

  2. Prefix every FSD layer with _ (_pages, _entities, _features, _widgets, _shared, ...) and keep app as the Next.js one. This keeps the original FSD layer names readable, gives a consistent "_ = FSD layer" rule, and lets the Next.js app stay under src/app without moving it out of src:

    src/
    ├── app/          # Next.js App Router (routing only)
    ├── _app/         # FSD app layer (providers / global config)
    ├── _pages/       # FSD pages layer
    ├── _widgets/     # FSD widgets layer
    ├── _features/    # FSD features layer
    ├── _entities/    # FSD entities layer
    └── _shared/      # FSD shared layer
    

    (There is no separate empty pages/ folder anywhere, and the only directory named app is the Next.js one.)

Both avoid adding an empty root pages/ folder, so the next/navigation hooks keep their non-null App Router signatures. These are just ideas, though — I'd be interested in what approach the maintainers would recommend here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions