# Why does my Next.js widget fail to load after a rename?

> Chimera CH-11 (Next.js, TypeScript): Why does my Next.js widget fail to load after a rename?

A dashboard page keeps its widgets in a config list and loads each one with ``dynamic(() => import(`../widgets/${name}`))``. The file name lives in the config as a string, and the import path is built from it when the slot renders. Rename `widgets/Churn.tsx` and no import in the project names it, so the rename has no importer to update and the slot fails when it loads. Write one literal `import()` per widget in a table keyed by a union of string literals, and type the config with that union.

The code below is from a small example project we wrote for this page. The break is shown by file and line, and we ran the project's own tests and type checks to confirm it.

## 1. The setup

`next/dynamic` is a good tool for this job. The Next.js documentation describes it as a composite of `React.lazy()` and Suspense, and it keeps a widget out of the initial JavaScript for the page. A dashboard has three widgets, a config list and one component that fills a slot.

```ts
// before/app/dashboard/widgets.config.ts
export const DASHBOARD_WIDGETS: string[] = ["Revenue", "Churn", "Signups"];     // line 1
```

```tsx
// before/app/dashboard/page.tsx
{DASHBOARD_WIDGETS.map((name) => (                  // line 8
  <WidgetSlot key={name} name={name} />             // line 9
))}

// before/components/WidgetSlot.tsx
export function WidgetSlot({ name }: { name: string }) {          // line 5
  const Widget = dynamic(() => import(`../widgets/${name}`));     // line 6
  return <Widget />;
}
```

The widgets are `widgets/Revenue.tsx`, `widgets/Churn.tsx` and `widgets/Signups.tsx`, each with a default export. Their numbers come from small functions in `lib/metrics.ts`, and `tests/metrics.test.ts` tests those functions directly.

## 2. Where the call path hides

- **The name is a string in two places.** `"Churn"` at `widgets.config.ts:1` is the only place the widget's file name appears in the app. `WidgetSlot.tsx:6` joins it to `../widgets/` at run time, so no line of the project reads `import("../widgets/Churn")`.
- **The import path is a template.** The Next.js documentation says that in `import('path/to/component')` the path must be explicitly written, and that it can't be a template string or a variable. The same note says the `import()` has to be inside the `dynamic()` call so Next.js can match bundles and module ids to that call and preload them, and that `dynamic()` can't be used inside React rendering because it needs to be at the top level of the module, similar to `React.lazy`. `WidgetSlot.tsx:6` is inside a render function, with a template.
- **A bundler can still do something with it.** The webpack documentation describes `import()` with a directory prefix and a dynamic part, using `./locale/${language}.json` as the example: every module that could be requested is included, so all `.json` files in that directory go into the new chunk. A dynamic request with a prefix is therefore something webpack knows how to bundle, and the widgets directory is the prefix here. The Next.js pages we read do not say what Next.js does with this exact line under webpack or under Turbopack, and we do not claim it.
- **The config is a `string[]`.** `DASHBOARD_WIDGETS: string[]` accepts `"Churn"` and `"Chrun"` alike. Nothing links an entry to a file.
- **A search for the file finds no importer.** A search for importers of `widgets/Churn` returns nothing, and a tool that lists unused files can report it as unused.

## 3. The edit that breaks it, and which call is left wrong

The widget was renamed to say what it measures. `widgets/Churn.tsx` became `widgets/Retention.tsx`, the component became `Retention`, and it now shows the share of customers kept, from `retentionRate` in `lib/metrics.ts`. The direct test, `tests/metrics.test.ts:10` to `:12`, was updated with it:

```tsx
// before/widgets/Retention.tsx (edit applied)
import { retentionRate } from "../lib/metrics";                // line 1 (was: churnRate)

export default function Retention() {                          // line 3 (was: Churn)
```

The edit is finished in the files it touches. No import of `widgets/Churn` existed, so the rename had no importer to rewrite. The compiler is given a `string[]` and a template and has no file named `Churn` to look for: with Next.js 16.4.3 installed, `tsc --noEmit` (TypeScript 5.9.3) reports nothing on `before/`.

The call left wrong is the string at `widgets.config.ts:1`. When `page.tsx:9` renders the second slot, `WidgetSlot.tsx:6` asks for `../widgets/Churn`, and no such module exists. The slot fails at that point, at run time, because the module cannot be found. The other two slots name files that exist.

The direct tests still pass, because they test functions in `lib/metrics.ts` and never load a widget. The case that catches it is `tests/widgets.test.ts:14`, written as a `todo` with the reason. It reads the config and checks that `widgets/<name>.tsx` exists for each entry, and the entry `Churn` has no file. We wrote it as a file check because loading a Next.js page in a test would need Next itself. With `node --test`, the `before/` suite reports 3 passed and 1 todo; the `after/` suite reports 6 passed.

A second edit has the opposite shape. Add a fourth file, `widgets/Latency.tsx`, and forget the config: nothing reports that the file is never shown. The same string is the only way in.

## 4. The fix

A table of literal imports, keyed by a union of string literals. Each `dynamic()` call is at the top level of the module with a path written out, as the Next.js note requires. The config is typed with the same union.

```diff
--- before/components/WidgetSlot.tsx
+++ after/components/WidgetSlot.tsx
 "use client";
 
 import dynamic from "next/dynamic";
+import type { ComponentType } from "react";
 
-export function WidgetSlot({ name }: { name: string }) {
-  const Widget = dynamic(() => import(`../widgets/${name}`));
+import type { WidgetKey } from "../widgets/keys";
+
+const WIDGETS = {
+  revenue: dynamic(() => import("../widgets/Revenue")),
+  retention: dynamic(() => import("../widgets/Retention")),
+  signups: dynamic(() => import("../widgets/Signups")),
+} satisfies Record<WidgetKey, ComponentType>;
+
+export function WidgetSlot({ name }: { name: WidgetKey }) {
+  const Widget = WIDGETS[name];
   return <Widget />;
 }
```

```diff
--- before/app/dashboard/widgets.config.ts
+++ after/app/dashboard/widgets.config.ts
-export const DASHBOARD_WIDGETS: string[] = ["Revenue", "Churn", "Signups"];
+import type { WidgetKey } from "../../widgets/keys.ts";
+
+export const DASHBOARD_WIDGETS: WidgetKey[] = ["revenue", "retention", "signups"];
```

```ts
// after/widgets/keys.ts
export const WIDGET_KEYS = ["revenue", "retention", "signups"] as const;

export type WidgetKey = (typeof WIDGET_KEYS)[number];
```

`page.tsx` is unchanged. The table costs one line per widget, and each of those lines is the importer a search or a rename looks for.

## 5. Why the fix works

- **Every widget file has an importer.** `WidgetSlot.tsx:9` to `:11` each write the path out. Rename `Retention.tsx` and a tool that follows imports has a specifier to rewrite at `:10`.
- **The name is a union, not a string.** The TypeScript handbook shows that combining literals into a union lets a function accept only a known set of values, and that an argument outside the set is an error at compile time. `WidgetKey` is that union, `DASHBOARD_WIDGETS` is `WidgetKey[]`, and `WidgetSlot` takes a `WidgetKey`. A config entry of `"churn"` is reported where it is written: on `after/`, tsc gives `TS2322: Type '"churn"' is not assignable to type '"revenue" | "retention" | "signups"'` at `widgets.config.ts:3`, and an import left pointing at `../widgets/Churn` gives `TS2307: Cannot find module` at that line of `WidgetSlot.tsx`.
- **The table must match the union.** `satisfies` validates that the type of an expression matches a type without changing the resulting type, per the TypeScript 4.9 release notes, and `Record<WidgetKey, ComponentType>` has one property for each key. A key with no entry, or an entry with no key, is a mismatch at the table. The release notes' own example shows the extra-key error. We checked the missing-key case: drop the `signups` line and tsc reports `TS1360` at the table, `Property 'signups' is missing`.
- **The loading rules are met.** The `dynamic()` calls are at module top level with explicit paths, which is the form the Next.js note describes. The lookup happens in render, and the `dynamic()` call does not.
- **The tests read the table.** `after/tests/widgets.test.ts:14` reads `WidgetSlot.tsx` as text and checks that each key has a literal `import()` of a file that exists. `:22` checks that no `import(` with a template is left. `:26` checks that each configured name is a key. We wrote them against the table's source because running it needs Next.

## 6. Rules that apply

- **AR-37**: in React and Next.js, don't call `import()` with a computed path for components agents edit. Keep one literal import per component, in a table keyed by a literal union.

## 7. References

Next.js (fetched 2026-10-31):
- Lazy loading, Pages Router (the note on explicit paths, template strings, `dynamic()` at module top level): https://nextjs.org/docs/pages/guides/lazy-loading
- Lazy loading, App Router (`next/dynamic` as a composite of `React.lazy()` and Suspense, `ssr: false` in Client Components, magic comments; the explicit-path note is not on this page): https://nextjs.org/docs/app/guides/lazy-loading

React (fetched 2026-10-31):
- lazy (declare at the top level of a module, not inside a component; `load` returns a promise of a module with a default export): https://react.dev/reference/react/lazy

webpack (fetched 2026-10-31):
- Module methods, dynamic expressions in `import()`: https://webpack.js.org/api/module-methods/

TypeScript (fetched 2026-10-31):
- TypeScript 4.9 release notes, the `satisfies` operator: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html
- Everyday types, literal types and unions of literals: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- Utility types, `Record<Keys, Type>`: https://www.typescriptlang.org/docs/handbook/utility-types.html

## Rules it tests

- [AR-37: Why does my agent miss dynamically imported components?](https://beta.llmgraph.in/agent-readable/ar-37.md)

Last verified 2026-10-31
