<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Walter Rafelsberger - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Walter Rafelsberger - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/walter-rafelsberger</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/walter-rafelsberger</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/walter-rafelsberger.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Tue, 15 Sep 2026 18:27:10 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Migrating 1,100 files to Redux Toolkit v2 without freezing the Kibana monorepo]]></title>
    <description><![CDATA[Kibana gave Redux Toolkit v2 the default package name and pushed v1 onto an explicit alias, which inverts the usual migration order. Webpack externals, yarn resolutions and an ESLint rule keep React Redux v7 and v9 out of each other's way.]]></description>
    <content:encoded><![CDATA[<p>We moved roughly 1,100 files in the Kibana monorepo onto <a href="https://github.com/elastic/kibana/pull/235577">Redux Toolkit (RTK) v2</a> aliases without asking a single plugin team to pause feature work. The usual migration pattern runs the other way around. Default package names (<code>@reduxjs/toolkit</code>, <code>react-redux</code>, <code>redux</code>) now resolve to v2, and existing v1 code sits behind explicit aliases, like <code>redux-toolkit-v1</code> and <code>react-redux-v7</code>. Both versions live in <code>node_modules</code> at once, kept apart at runtime by npm aliases and webpack module replacement. An ESLint rule scoped to 36 plugin paths catches anything that tries to cross. When a team is ready, it deletes its path from that list and switches back to the default imports, and the teams around it carry on shipping.</p><h2>Why upgrade to Redux Toolkit v2?</h2><p>RTK v2 was released in late 2023. That's nearly three years of running on a major version behind in one of the most widely used state management libraries in the JavaScript ecosystem. It reflects how hard this upgrade is in a codebase of Kibana's size. A <a href="https://github.com/elastic/kibana/pull/178986">previous attempt</a> tried the big-bang approach and stalled when the real scope became clearer. </p><p>So what does v2 actually bring? It ships alongside Redux core 5.0, React-Redux 9.0, Reselect 5.0, and Redux Thunk 3.0. React-Redux 9.0 requires React 18 and drops the <code>useSyncExternalStore</code> shim that v8 carried for React 16/17. Since Kibana already runs React 18, upgrading sheds legacy compatibility code and keeps Kibana on the actively maintained Redux majors.</p><p>RTK v2 also brings genuinely useful new features, including inline selectors in <code>createSlice</code> and opt-in inline async thunks through a customized <code>buildCreateSlice</code> setup, along with a <code>combineSlices</code> API with slice reducer injection for code splitting. That last one is particularly interesting for Kibana's plugin architecture where lazy-loading is the norm.</p><h2>How Redux is used across the Kibana monorepo</h2><p>Before diving into the solution, it's worth understanding just how varied Redux usage is across Kibana. A full audit of the codebase (tracked in <a href="https://github.com/elastic/kibana/issues/239863">#239863</a>) revealed several distinct camps:</p><p><strong>Pattern</strong></p><p><strong>Plugins and packages</strong></p><p><strong>What the migration needs</strong></p><p>Redux Toolkit v1</p><p>Discover, Lens, Synthetics, Security Solution</p><p>Full v1 to v2 migration</p><p>Plain Redux v4</p><p>Canvas, Maps, Index Management, Cross-Cluster Replication</p><p><code>redux-v4</code> alias only, no RTK migration</p><p>Kea</p><p>Enterprise Search (150+ files), Content Connectors</p><p><code>react-redux-v7</code> alias, no RTK migration</p><p><code>redux-saga</code></p><p>Synthetics, Graph, Uptime</p><p>Store setup only, saga is version-independent</p><p><code>typescript-fsa</code></p><p>Security Solution data-table package</p><p>Out of scope</p><p>Types and single imports</p><p>Expressions, Monitoring</p><p>Alias swap only</p><p>Plugins such as Discover, Lens, Synthetics, and Security Solution use RTK v1 APIs,including <code>createSlice</code>, <code>configureStore</code>, <code>createAsyncThunk</code>, and <code>createSelector</code>. These are the ones that actually need the v1 to v2 migration. But even here, complexity varies wildly. Lens uses stand-alone <code>getDefaultMiddleware</code> (removed in v2) and <code>PreloadedState</code> (also removed). Security Solution is the largest consumer at 300+ files, mixing modern RTK with legacy plain Redux patterns.</p><p>Canvas, Maps, Index Management, Cross-Cluster Replication, and several others still use plain Redux v4via <code>createStore</code>, <code>combineReducers</code>, <code>applyMiddleware</code>, and <code>connect</code>, which are classic patterns from the pre-RTK era. These don't need RTK migration at all since they're not using it  in the first place, but they do need the <code>redux-v4</code> alias since the default <code>redux</code> package is now v5.</p><p>Enterprise Search and Content Connectors use <code>kea</code>, a Redux abstraction layer with its own logic builders (<code>kea()</code>, <code>useValues</code>, <code>useActions</code>). There are more than 150 files in Enterprise Search alone. RTK migration isn't applicable here, since <code>kea</code> is its own world. But it <em>does</em> depend on <code>react-redux</code> v7 under the hood, which is where the bundler tricks come in.</p><p>Synthetics, Graph, and Uptime use <code>redux-saga</code> for side effects. Saga integration is actually independent of the RTK version, but these plugins need their store setup migrated.</p><p>The Security Solution data-table package uses <code>typescript-fsa</code> and <code>typescript-fsa-reducers</code> instead of RTK entirely, with its reducer embedded into Security Solution's main store, and isn’t part of the RTK migration at all.</p><p>The Expressions plugin only imports <code>shallowEqual</code> from <code>react-redux</code>, and Monitoring only imports types. These just need an alias swap.</p><p>Asking every team to migrate simultaneously was a nonstarter. The breaking changes in RTK v2 include stricter type checking and removed APIs, like <code>enableES5()</code> from immer, <code>getDefaultMiddleware</code> and <code>PreloadedState</code> gone entirely, <code>AnyAction</code> replaced by <code>UnknownAction</code>, and behavioral changes in how middleware is configured.</p><h2>Running Redux Toolkit v1 and v2 side by side</h2><p>The solution was to flip the typical migration pattern on its head. Instead of keeping the default imports on v1 and introducing v2 under aliases, the default package names (for example, <code>@reduxjs/toolkit</code>, <code>react-redux</code>, and <code>redux</code>) now point to v2. The old versions live under versioned aliases:</p><ul><li><p><code>redux-toolkit-v1</code></p></li><li><p><code>react-redux-v7</code></p></li><li><p><code>redux-v4</code></p></li><li><p><code>immer-v9</code></p></li><li><p><code>reselect-v4</code></p></li><li><p><code>redux-thunk-v2</code></p></li></ul>{
"@reduxjs/toolkit": "2.12.0",
"redux-toolkit-v1": "npm:@reduxjs/toolkit@1.9.7",
"react-redux": "9.2.0",
"react-redux-v7": "npm:react-redux@7.2.8"
}<p>This is npm's alias syntax. <code>"react-redux-v7": "npm:react-redux@7.2.8"</code> installs the old version under a different name. Both versions coexist in <code>node_modules</code> without conflicts.</p><p>The insight here is that all existing code in this pull request (PR) was moved to v1 aliases. Every <code>import { useSelector } from 'react-redux'</code> became <code>import { useSelector } from 'react-redux-v7'</code>. That's ~1,100 files touched, but the vast majority (~1,000) are mechanical one-liner import swaps. When a team is ready to migrate to v2, they switch back to the default import names. Once all v1 aliases disappear from the codebase, the old packages can be removed entirely.</p><p>This avoids the alternative, where v2 imports would end up under nonstandard names permanently, leaving nonstandard imports in the codebase for the long term.</p><h2>Serving both versions through the bundler</h2><p>Getting two versions of the same library to coexist at runtime is where things got interesting. Kibana uses <code>kbn-ui-shared-deps-npm</code> to bundle common dependencies as shared webpack externals. This needed to serve both the new v2 packages <em>and</em> the v1 aliases so that both are available at runtime.</p><h3>Pinning @elastic/charts with yarn resolutions</h3><p>Then there's <code>@elastic/charts</code>. It depends on RTK v1 internally and can't just be upgraded independently since it's an upstream package. Yarn resolutions pin its nested dependencies to v1 versions:</p>{
"@elastic/charts/@reduxjs/toolkit": "npm:@reduxjs/toolkit@1.9.7"
}<p>A <code>NormalModuleReplacementPlugin</code> in the shared deps webpack config detects when an import of <code>immer</code>, <code>@reduxjs/toolkit</code>, <code>redux</code>, <code>react-redux</code>, or <code>reselect</code> originates from within <code>@elastic/charts</code> and redirects resolution to the nested v1 copies. This ensures that <code>@elastic/charts</code> resolves to its compatible v1 dependency set.</p><h3>Keeping Kea on React Redux v7 with webpack externals</h3><p>The <code>kea</code> library was another fun case. It declares <code>react-redux</code> as a peer dependency (<code>&gt;= 7</code>), so without special handling its imports resolve to Kibana's default v9 package. The migration keeps Kea consumers on <code>react-redux-v7</code>, so Kea must use that same React context. The fix uses function-based webpack/rspack externals that skip externalizing <code>react-redux</code> when the import comes from <code>node_modules/kea</code>, combined with a <code>NormalModuleReplacementPlugin</code> that rewrites it to <code>react-redux-v7</code>. This ensures that <code>kea</code> uses the v7 React context that matches the <code>&lt;Provider&gt;</code> wrapping its consumers.</p><p>Both the webpack (<code>kbn-optimizer</code>) and the rspack (<code>kbn-rspack-optimizer</code>) configs needed these changes, with a shared <code>isKeaReactReduxImport</code> helper extracted to keep the logic consistent.</p><h2>Using an ESLint rule to prevent cross-version imports</h2><p>With two versions available, accidental cross-version imports are the biggest risk. A new <code>@kbn/imports/no_redux_toolkit_v2_imports</code> ESLint rule catches any import of the v2 default packages (such as <code>@reduxjs/toolkit</code>, <code>react-redux</code>, or <code>redux</code>, among others) in code that hasn't been migrated yet. It even auto-fixes them to the v1 aliases for file imports and Jest mock paths.</p><p>The rule is scoped via an override in <code>.eslintrc.js</code> to the ~36 plugin and package paths currently using v1. When a team migrates, they simply remove their path from the override list. This clean, self-service approach requires no coordination.</p>// .eslintrc.js (simplified)
overrides: [{
  files: [
'src/platform/plugins/shared/discover/**/*.{ts,tsx}',
'src/platform/plugins/shared/workflows_management/**/*.{ts,tsx}',
// ... 34 more paths
],
  rules: {
'@kbn/imports/no_redux_toolkit_v2_imports': 'error',
  },
}]<h2>Why mixing React Redux v7 and v9 breaks the context</h2><p>This is worth calling out because it's an easy failure mode to miss during an upgrade. <code>react-redux</code> v9 and v7 create separate React contexts. If a component tree has a v9 <code>&lt;Provider&gt;</code> at the top but a child component calls <code>useSelector</code> from v7 (or vice versa), React-Redux cannot find the matching context. In development, it throws an error explaining that the component must be wrapped in a matching <code>&lt;Provider&gt;</code>; in production, the missing context causes a runtime error when the hook accesses the store.</p>Error: could not find react-redux context value; please ensure the component is wrapped in a &lt;Provider&gt;<p>This means that each plugin needs to be explicitly pinned to one version. Shared packages that use <code>react-redux</code> can only be consumed by code on the same version, since mixing isn't possible. This is a constraint that makes the migration inherently per plugin rather than per file.</p><h2>Migration batches: What can move independently</h2><p>The dual-version setup gives every team a clear path forward, and the dependency graph analysis from the tracking issue identified natural migration batches:</p><ul><li><p><strong>Batch 1: Independent, self-contained stores.</strong> Packages like <code>kbn-coloring</code>, <code>transform</code>, <code>timelines</code>, and <code>expandable-flyout</code> have fully internal Redux stores with no types leaking through their public APIs. These can be migrated independently by their owning teams, with minimal risk.</p></li></ul><ul><li><p><strong>Batch 2: Coupled packages.</strong> Some packages share RTK types across boundaries and <em>must</em> migrate together. The machine learning (ML)/artificial intelligence for IT operations (AIOps) chain is one example: <code>@kbn/ml-response-stream</code> exports a <code>streamSlice</code> (a <code>createSlice</code> return value) that <code>@kbn/aiops-log-rate-analysis</code> embeds directly into its <code>configureStore</code>. Migrating one without the other causes type mismatches between v1 and v2 slice types. Similar coupling exists across the Lens ecosystem. The Lens plugin depends on <code>@kbn/coloring</code> (which has its own RTK store), <code>@kbn/lens-embeddable-utils</code>, and <code>@kbn/lens-common</code>, while itself being consumed by 40+ packages and plugins across chart expressions, visualizations, Maps, Canvas, and observability plugins. Whether Redux types leak through a package's public API determines if it can be migrated independently or needs coordination. <code>kbn-coloring</code>'s store is internal to its React components so it's safe to migrate alone, but other coupling points need careful analysis.</p></li></ul><ul><li><p><strong>Batch 3+: The big ones.</strong> Discover, Security Solution, and Lens each have their own migration timelines. Security Solution's 300+ files and mix of RTK with plain Redux v4 and <code>typescript-fsa</code> make it the largest effort, but the different patterns can be addressed independently. Lens has the trickiest v2 breaking changes around middleware configuration; stand-alone <code>getDefaultMiddleware</code> and <code>PreloadedState</code> are both removed in v2, and it has four custom middleware files with complex typing.</p></li></ul><p>Beyond the batched migrations:</p><ul><li><p><strong>Deprecated features</strong> can stay on v1 aliases. When the feature is removed, the v1 imports disappear through code deletion, without any migration work.</p></li><li><p><strong>Plain Redux v4 plugins</strong> (Canvas, Maps, and others) are entirely out of scope for RTK migration. They'd benefit from modernization, but that's a separate initiative.</p></li><li><p><strong>Kea plugins</strong> need <code>react-redux-v7</code> to <code>react-redux</code> alias updates eventually, but no RTK migration. The longer-term question (whether to keep Kea or migrate to RTK v2) is a separate decision.</p></li><li><p><strong>The dual-version approach</strong> adds measurable bundle overhead during the transition. It’s a trade-off but is acceptable for the migration period.</p></li></ul><h2>Lessons for other large monorepo upgrades</h2><p>The ESLint rule turned out to be the linchpin. Without automated enforcement, aliased imports would drift back to default names within weeks. With it, the migration state is visible in the paths listed in the override. As of the initial PR, zero files import from <code>@reduxjs/toolkit</code> v2. Every RTK usage goes through the <code>redux-toolkit-v1</code> alias. That's the starting line.</p><p>The preparation work also reached beyond import paths. Jest mocks referencing <code>react-redux</code> needed updating to <code>react-redux-v7</code>, as did Storybook previews, test helpers, and ambient type declarations. Multiple rounds of <code>node scripts/eslint_all_files --no-cache --fix</code> caught the mechanical cases; the remaining cases needed manual fixes.</p><p>If you're facing a similar major dependency upgrade in a large monorepo, the pattern of giving the new version the default name and the old version an explicit alias is worth considering. New code naturally uses the current version, while older usage stays visible and trackable until it reaches zero.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/redux-toolkit-v2-migration-kibana-monorepo</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/redux-toolkit-v2-migration-kibana-monorepo</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[Kibana]]></category>
    <dc:creator><![CDATA[Walter Rafelsberger]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3fa3c79a7e246c89/6a9111235c3126655043df06/unnamed.png" length="0" type="image/png"/>
    <pubDate>Fri, 28 Aug 2026 15:20:00 GMT</pubDate>
  </item>
  </channel>
</rss>