For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Frameworks

Why does my agent miss dynamically imported components?

Rule AR-37

In React and Next.js, don't call import() with a computed path for components agents edit.

import("./widgets/" + name) names no file. A search for Chart.tsx's importers finds nothing, and a rename of the file breaks the page at runtime. Write one import("./widgets/Chart") per component, in a table keyed by a literal union, and every file has a visible importer.

Why it happens

MDN's page on import() states that dynamic imports "allow any expression as the module specifier, not necessarily string literals." Its use case is when the specifier "needs to be constructed dynamically", which static import cannot do. The same page advises: "Use dynamic import only when necessary. The static form is preferable for loading initial dependencies, and can benefit more readily from static analysis tools and tree shaking."

The static declaration is the opposite design. MDN says import declarations "are designed to be syntactically rigid (for example, only string literal specifiers, only permitted at the top-level ...), which allows modules to be statically analyzed and linked before getting evaluated." Resolving a specifier is left to the host: the ECMAScript specification "does not define how module specifiers are resolved", and bundlers define their own rules on top.

Next.js documents its rule for next/dynamic: "In import('path/to/component'), the path must be explicitly written. It can't be a template string nor a variable." The import also has to be inside the dynamic() call "for Next.js to be able to match webpack bundles / module ids to the specific dynamic() call and preload them before rendering," and dynamic() cannot be used inside React rendering since it must be at the top level of the module, similar to React.lazy. The docs describe next/dynamic as a composite of React.lazy() and Suspense.

So there are two separate costs, and both come from a path that isn't a literal:

  • For the framework: the bundler can't tell which files a computed specifier may load.
  • For an agent: a literal path is a string a search can match. A computed one is built from parts, so the file name never appears in the importer.

Next.js also documents magic comments such as webpackIgnore: true and turbopackIgnore: true, which leave the import expression as-is in the output and are described as "useful for runtime-only modules." That is a deliberate opt-out of bundling, and it is the same visibility trade as a computed path.

Bad and good

A dashboard page shows widgets chosen by name.

typescript// bad: the path is built at runtime import dynamic from "next/dynamic"; export function Widget({ name }: { name: string }) { const Component = dynamic(() => import(`./widgets/${name}`)); return <Component />; }

This breaks two Next.js rules at once: the path is a template string, and dynamic() runs during rendering. It also hides the three files in widgets/. Renaming Chart.tsx to LineChart.tsx changes nothing the compiler can see.

typescript// good: one literal import per widget, in a table keyed by a literal union import dynamic from "next/dynamic"; import type { ComponentType } from "react"; type WidgetName = "chart" | "table" | "summary"; const widgets = { chart: dynamic(() => import("./widgets/Chart")), table: dynamic(() => import("./widgets/Table")), summary: dynamic(() => import("./widgets/Summary")), } satisfies Record<WidgetName, ComponentType>; export function Widget({ name }: { name: WidgetName }) { const Component = widgets[name]; return <Component />; }

Each dynamic() call is at module top level with a literal path, as the Next.js docs require. Every widget file now has an importer, WidgetName is a union a caller must satisfy, and satisfies Record<WidgetName, ...> makes the compiler report a missing entry (see AR-07 for the satisfies behaviour).

Trade-offs and when a computed path is acceptable

  • The table is more lines. Each widget costs one entry. That entry is the visible importer.
  • Plugins and user-supplied modules can't be listed ahead of time. Keep those loads in one small module and name the contract (AR-09), and don't let that module spread into the rest of the app.
  • Loading a library on demand (await import("fuse.js") in an event handler, as the Next.js guide shows) uses a literal and is fine.
  • Server Components: the Next.js App Router guide says ssr: false is not supported in Server Components, and that when a Server Component dynamically imports a Client Component, automatic code splitting is currently not supported. Put the table in a Client Component when you need ssr: false.
  • Runtime-only imports with webpackIgnore or turbopackIgnore are a deliberate choice. Mark them with a comment saying where the targets live.

Enforce it

ToolSettingWhat it catchesChecked
eslintno-restricted-syntax with a selector for the ESTree node ImportExpression and an attribute filter such as [source.type!='Literal']import() whose argument is not a plain string literal. The rule takes ESTree selectors (documented), and the attribute filter syntax is shown on the rule page. Checked with eslint 9.39.5: this selector flags import() calls with a non-literal argument and leaves a plain string-literal import() clean.2026-10-30
typescript-eslintno-explicit-any (in recommended)any used to type the result of a computed import2026-10-30
tsconfigstrictan implicit any on a module loaded by variable2026-10-30

In eslint.config.js:

js{ rules: { "no-restricted-syntax": [ "error", { selector: "ImportExpression[source.type!='Literal']", message: "Write the module path as a string literal.", }, ], }, }

Checked with eslint 9.39.5: a template-string import() is flagged. A template literal with no expressions has the node type TemplateLiteral, so it is also flagged by this selector. Whether you want that is a project choice.

Rule block for AGENTS.md / CLAUDE.md

markdown- Write `import()` and `dynamic(() => import("..."))` with a string literal path. Never build the path from a variable or template string. - To load one of several components by name, add a table: `{ chart: dynamic(() => import("./widgets/Chart")), ... }` keyed by a literal union type, and look the component up in it. - Call `dynamic()` at module top level, never inside a component. - When you add or rename a file in a directory that has such a table, edit the table in the same change.

Edge cases

  • A file only reachable by a computed path has no importer. Dead-code checks and "who imports this" searches report it as unused, and a cleanup pass may delete it. That is why CH-06 and CH-11 are worth reading before tidying.
  • **React.lazy** takes the same form, a function that returns import("..."). Next.js notes that dynamic() must be marked at the top level of the module "similar to React.lazy." Use a literal there too.
  • Named exports: import("../components/hello").then((mod) => mod.Hello) is the documented form and keeps the path literal.
  • Pages Router and App Router share next/dynamic and "behave the same way," per the docs, so the rule is the same in both.
  • CommonJS: the same pattern with require(variable) is covered in AR-38.

References

  • MDN, import(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
  • MDN, import declaration: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
  • Next.js, lazy loading (App Router): https://nextjs.org/docs/app/guides/lazy-loading
  • Next.js, lazy loading (Pages Router): https://nextjs.org/docs/pages/guides/lazy-loading
  • ESLint no-restricted-syntax: https://eslint.org/docs/latest/rules/no-restricted-syntax
  • typescript-eslint no-explicit-any: https://typescript-eslint.io/rules/no-explicit-any/
  • TSConfig reference: https://www.typescriptlang.org/tsconfig/

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: tool checks resolved (eslint 9.39.5, tsc 7.0.2, pyright 1.1.414).
  • 2026-10-30: remaining checks closed (claims reduced to documented behaviour)
  • AR-05 Why do fallback imports make agents edit the wrong code?
  • AR-07 Why does my coding agent miss calls made through getattr?
  • AR-38 How should Express routes be organised for coding agents?

Last verified

For agentsthis page as Markdown/agent-readable/ar-37.mdllms.txt