Update and shrink the `publish-ci` example set
(AI writeup result from my own research session)
Summary
The examples/publish-ci/ set has not had a real review since it was built in 2023. Recent commits on those directories are dependency bumps and chore: changes, not maintenance. The result is that we are spending CI time on toolchains that no longer represent the ecosystem, several examples test the same resolution path as each other, and two of our shipped build artifacts have no coverage at all.
This issue proposes cutting the set from 8 examples to 5, modernizing what remains, and fixing the CI configuration problems found along the way.
These examples are also consumed by React-Redux, Redux core, and Reselect, so most changes need a coordinated rollout. See Coordinating across repos at the bottom.
Current state
| Example | Toolchain | Current release | Resolves RTK via |
|---|---|---|---|
cra4 |
react-scripts 4, Webpack 4 |
dead since 2022 | module field → legacy-esm.js |
cra5 |
react-scripts 5.0.1, Webpack 5 |
last published Jan 2022 | exports module → modern.mjs |
next |
Next 13.2, Pages Router | Next 16.3.0 | exports module → modern.mjs |
vite |
Vite 7 | Vite 8.2.1 | exports module → modern.mjs |
node-standard |
Node, CJS package | — | require → dist/cjs/index.js |
node-esm |
Node, "type": "module" |
— | same assertions as node-standard |
react-native |
RN 0.79.2 | RN 0.86.2 | react-native condition or field |
expo |
Expo SDK ~53 | Expo SDK 57.0.11 | same as react-native |
All four web examples still declare @reduxjs/toolkit: ^2.0.0-rc.3, react-redux: ^9.0.0-rc.0, and msw: ^1.3.2. They pull release-candidate ranges from 2023.
The three examples/type-portability/ projects (bundler, nodenext-cjs, nodenext-esm) are in better shape and are not proposed for changes here. They were built deliberately to prevent TypeScript type-portability regressions and they still do that job.
Problems found
1. Three web examples test one resolution path. cra5, next, and vite all resolve to dist/redux-toolkit.modern.mjs. RTK lists module-sync and module ahead of browser and import in its exports map, and Webpack 5, Next, and Vite all offer a module condition, so all three match at the same key. They differ in bundler internals — Babel vs SWC vs esbuild — but not in which RTK build they consume.
2. node-standard and node-esm assert the same things. Their test files are byte-identical apart from the CJS filename. Both run node --no-experimental-require-module test-cjs.* && node test-esm.mjs, so both cover CJS-require and ESM-import in one process each. The "type": "module" difference does not change which files get resolved.
3. require(esm) is not tested at all. Both Node examples pass --no-experimental-require-module, which suppresses the module-sync condition. On Node 22.12+ and 23+, require('@reduxjs/toolkit') matches module-sync and resolves to redux-toolkit.modern.mjs, not dist/cjs/index.js. That is the single biggest untested path in the package today, and it is the one most likely to affect real users.
4. dist/redux-toolkit.browser.mjs is reached by nothing. It is built, published, and referenced by exports["."].browser and the unpkg field, but browser is listed after module-sync and module, so no bundler ever matches it. (Separately: that artifact is intended as a script-tag / import-map replacement for the old UMD build, so pointing a bundler condition at it is arguably wrong. Tracking that in #5359.)
5. cra4 is the only thing reaching RTK's module field, and it is currently broken. In the React-Redux signals prototype branch it fails with:
./node_modules/react-redux/dist/react-redux.legacy-esm.js
Cannot find module: 'alien-signals'. Make sure this package is installed.[email protected] ships an exports map with no main and no module field. Webpack 4 has no package.json exports support — that landed in Webpack 5.0.0, per the release post — so it cannot resolve the package at all.
6. examples/type-portability/** is missing from the paths-filter in tests.yml. A PR that touches only those examples skips CI entirely. This is a straight bug, independent of everything else here.
7. About 400 lines of application code are copied into every web example. cra4, cra5, and vite are byte-identical across all 15 shared source files. next differs only because its folder is named src/app-core/ instead of src/app/; the five differing files differ solely in import paths. There is no framework-specific application code in any of them.
8. Smaller CI issues.
NODE_OPTIONS: --openssl-legacy-provideris set for every job in the matrix. Onlycra4needs it.- Reselect sets up JDK 17 only when
matrix.example == 'react-native', so theexpojob gets no JDK. - RTK runs the matrix on Node 24 only. React-Redux and Reselect run Node 22 only. No repo tests both.
build:examplesexists as a root script but no workflow calls it.
Plan
Proposed lineup: 8 → 5
| Example | Action | Why |
|---|---|---|
next |
Keep. Bump to Next 16, switch to App Router. | Covers the framework most likely to break on packaging changes. App Router exercises RSC, which the Pages Router example does not. Apply the useState store initializer while we're in there. |
vite |
Keep. Bump to Vite 8. | Covers Rollup-based resolution and is the default for most new apps. |
node |
Keep, merged from node-standard + node-esm. Add a require(esm) test mode. |
Same coverage as the two current examples in half the jobs, plus the largest current gap. |
react-native |
Keep. Bump to RN 0.86. | Only thing covering the react-native export condition. |
expo |
Keep. Bump to SDK 57. | Metro's exports handling differs from bare RN's. Worth keeping both. |
cra5 |
Delete. | Zero unique coverage. react-scripts has not shipped since Jan 2022. |
cra4 |
Delete. | See below. |
esbuild |
Optional, new. | The only proposal that would cover an artifact nothing else touches. Blocked on resolving the browser condition question in #5359 first — otherwise it would land on the minified production build and lock that in as expected behavior. |
Across all four repos this takes the number of test-published-artifact jobs from 32 down to 20, or 24 with esbuild.
On deleting cra4 and legacy-esm
Deleting cra4 means nothing exercises RTK's module field or dist/redux-toolkit.legacy-esm.js anymore. That is an acceptable trade:
- The
modulesetup works today, and package structure is frozen for the rest of 2.x. A regression test protects against change, and there is no change coming to protect against. legacy-esmkeeps shipping in 2.x. Removing it mid-major would break anyone still on a pre-exportsbundler.legacy-esmand themodulefield get deleted together in RTK 3.
One thing would reopen this: if RTK itself takes on an exports-only dependency the way React-Redux did with alien-signals. At that point Webpack 4 is broken regardless of what the module field says, and shipping legacy-esm becomes a false promise.
Also in scope
- Add
examples/type-portability/**to thepaths-filterintests.yml. - Add a Node 22 entry alongside Node 24 in
test-published-artifact. - Drop
NODE_OPTIONS: --openssl-legacy-provideroncecra4is gone. - Shrink the duplicated example app. It exists to give the bundler something to resolve — it does not need a counter feature, a posts feature, MSW, and CSS modules to prove that all four entry points resolve. Roughly 60 lines covers the same paths. Note that a shared workspace package is not an option: each example has its own lockfile and is deliberately excluded from the root
workspacesarray, so that it resolves RTK from a registry tarball the way a real user does. Adding aworkspace:link would change the thing under test and would break the downstream repos, which runyarninside a single example directory with no root install. - Rename
src/app/→src/app-core/in the remaining web examples, sonextstops diverging. Next's App Router ownssrc/app/, soapp-coreis the correct name after the App Router switch anyway.
Not in scope
- The 19 apps under
examples/query/react/andexamples/action-listener/. They are much staler and 16 of them are embedded in the docs as CodeSandboxes. Separate problem, separate issue. - Rspack and React Router examples. Rspack is a Webpack-compatible resolver and would duplicate what
nextandvitecover. React Router apps are Vite-based, whichvitealready covers. - Anything requiring RTK 3: deleting
legacy-esm, deleting the CJS build, droppingnodenext-cjs, raising the minimum Node version.
Coordinating across repos
Three other repos clone this repo at a pinned SHA and run the same example matrix. Any change to the matrix requires an edit in each of them.
| Repo | File | Matrix location | RTK pin | Node |
|---|---|---|---|---|
reduxjs/redux-toolkit |
.github/workflows/tests.yml |
~line 219 | n/a | 24.x |
reduxjs/react-redux |
.github/workflows/test.yml |
lines 155–165 | 576a02f8 (v2.12.0, 2026-05-15) |
22.x |
reduxjs/redux |
.github/workflows/test.yaml |
lines 201–212 | 7c49510f (2026-06-02) |
24.x |
reduxjs/reselect |
.github/workflows/build-and-test-types.yml |
lines 140–149 | 576a02f8 |
22.x |
Because the downstream repos pin a SHA, deleting an example from RTK's master does not break them immediately. It breaks the next time someone bumps the pin. So the safe order is:
- Land the example changes in RTK.
- Update the matrix in each downstream repo.
- Bump the pin in each downstream repo.
Doing 3 before 2 breaks all three downstream CIs.
Medium-term fix worth considering: publish the example matrix as a single JSON file in this repo and have the downstream workflows read it after cloning. The list then lives in one place and the four-file edit goes away permanently.
Source: reduxjs/redux-toolkit