Redux Toolkit Migration at Shopify POS: Cutting Boilerplate Without a Rewrite
Redux remains a polarizing topic in the React ecosystem. Its reliability is rarely questioned, but the verbosity it introduces often is. Redux Toolkit (RTK) was built by the Redux maintainers to address that complaint directly—generating action creators automatically, bundling middleware like Redux-Thunk, and offering a mutable syntax that is far easier to read than the traditional immutable style.
Late in 2022, Shopify’s Point of Sale (POS) team decided to revisit state management in their React Native app. The codebase had been running on Vanilla Redux for roughly two years, and developers were spending too much time tracing bugs through layers of action creators and type definitions. Rather than starting from scratch, the team wanted a migration path that simplified the code while preserving the unidirectional flow and tooling they already depended on.
Choosing a Migration Strategy
The team assembled a task force of developers from different areas of the POS app and weighed two approaches. The first was a full rewrite of all state management to make it perfectly idiomatic RTK. That promised maximum code savings and an optimally configured setup, but carried significant risk: the task force was made up of generalists, not specialists in every corner of the app, and complex code owned by more specialized teams could easily be destabilized. Estimating the effort would also have been harder.
The second approach was a balanced migration: adopt most RTK features but leave the most complicated logic in a style closer to Vanilla Redux. The trade-offs were acceptable—less total code reduction than a rewrite, but faster turnaround, easier estimation, and lower risk because much of the logic could be reused. The team chose this path, and it ultimately only affected how Thunks were rewritten.
Rolling Out Reducer by Reducer
The migration itself took about three months and was surprisingly seamless, thanks largely to RTK’s reverse compatibility with Vanilla Redux. Vanilla reducers and RTK slices can coexist in the same root reducer, which allowed the team to refactor one reducer at a time. Each migrated reducer was merged to the main branch and released to production individually. If a new slice broke something, the blast radius was small and easy to roll back.
In most places, the team swapped the Vanilla immutable syntax for RTK’s mutable syntax. The addMessage and updateMessage mutations in an RTK slice are not only clearer but also more compact, because the slice generates the action creators automatically.
The Exception: Async Logic
Where the implementation diverged from ideal RTK usage was in asynchronous code. RTK Query, which handles web requests, was not an option because it lacks GraphQL support at this time. The alternative—createAsyncThunk with its generated pending and fulfilled actions—was appealing but presented a problem: moving wholesale to those actions would split logic between the Thunk and the reducer, forcing a substantial rewrite.
The team opted for a hybrid approach. They kept state contained inside reducer cases that needed to be dispatched independently, and used the ThunkAPI from createAsyncThunk for now. This removed the risk of destabilizing the app during migration. Teams were left to adopt the more idiomatic builder method and generated actions in separate projects later.
Lessons Learned
The results spoke for themselves. By the end of the migration, the team deleted 3,500 lines of boilerplate code. Developers were enthusiastic about returning to their home teams and building on the new foundation.
Several gotchas are worth noting for anyone planning a similar refactor:
- Type your hooks immediately. Explicitly typing
useDispatch,useSelector, anduseStorefrom the start prevents a flood of TypeScript warnings late in the project. - Use
unwrapwhen awaiting dispatch calls. Unlike Vanilla Redux,dispatchno longer returns the dispatched value.dispatch().unwrap()replicates the oldawait dispatch()behavior.
Some reducers translated awkwardly into RTK. In most cases, those were reducers that already deviated from Redux best practices in their Vanilla form. The team handled these with separate subprojects on a case-by-case basis. If a codebase contains many such reducers, it may be wise to address those patterns before starting the migration.
The migration proved that RTK is a practical way to reuse existing Redux knowledge and code while modernizing state management. Removing boilerplate makes the architecture easier to understand and debug, and new features no longer require developers to write repetitive setup code. The team is now building on its RTK base with enthusiasm.



