How Migrating from Vanilla Redux to Redux Toolkit Improved State Management in Shopify POS

State Management is a contentious subject in the React community. A lot of debates focus on Redux and whether we should still use it or not. In fact, some reading this may have had disappointing experiences with Redux and sworn off trying it ever again. For those of you who haven’t used React before, Redux is a library that helps manage the global state of your application. Although it's extremely reliable, it's been criticized in the past for creating overly verbose code with a lot of boilerplate. In recent years, the maintainers of Redux have come out with a new library called Redux Toolkit (RTK) that addresses most, if not all, of the common complaints. My favorite highlight is that RTK generates action creators for you, allowing the removal of A TON of boilerplate. The other change I loved is that it has middleware, like Redux-Thunk, included so you don’t have to do as much setup.

The focus of this blog isn’t why you should use RTK in your next project; it's focused on our experience of migrating Shopify’s Retail Point of Sale app from Vanilla Redux to RTK. The reality is, whatever your stance on Redux, there’s a ton of code already written out there that uses it. I’m hoping that our experience can inform your decision on whether or not to take the leap and migrate your Vanilla Redux codebase to RTK.

Why Did We Choose Redux Toolkit?

Near the end of 2022, the Point of Sale (POS) team decided that we wanted to take a second look at state management in our React Native app. The state management library used then was the Vanilla Redux most of us are already familiar with. It was in production for about two years, and developers were unhappy managing the typical boilerplate. If there was a bug, you would then need to wade through all the different action creators and types until you could figure out what was actually causing the problem.

Since a lot of this logic was really complex, we didn’t want to rewrite everything from scratch. We wanted a solution that allowed us to pare things down while reusing some of the code. We came up with the idea to use Redux Toolkit. In RTK, you can maintain the same unidirectional flow and tooling you enjoy with Vanilla Redux, but get the benefits of a more terse and easier to read syntax.

What Was Our Migration Strategy?

At the beginning of the project, developers from various parts of the POS app acted as a task force to complete the migration. Armed with this task force and the decision to use RTK, we considered two strategies for migrating over. The first was a full rewrite of all state management to make it perfectly idiomatic to RTK. The second was a balanced approach where we migrated to most of the RTK features, but left the code that was more complex to refactor in a similar style to Vanilla.

The benefits of the first approach would have been

  • maximum code savings
  • no need to make future RTK related projects
  • most optimized setup for RTK state management
  • greatly increased readability of state management code.

The benefits of the second approach were

  • easy to estimate
  • fastest project turnaround time because of reused logic
  • least risk of destabilization because we reused more code
  • large amount of code reduction although less then a full rewrite
  • greatly increased readability of state management code.

We opted to not go for the first approach because the task force was a subset of Shopify developers that weren’t necessarily specialized in all areas of the app. We didn’t want to break complex code being used by teams that were more specialized. Also, it also would have taken longer and would have been more complex to estimate.

The second approach was better for us because it allowed a team of generalists to come in and greatly simplify our Redux implementation without the fear of destabilizing the app. If teams wanted to further optimize their RTK setup, they could create their own separate projects. In the end, this decision really only affected how we rewrote our code for Thunks, but more about that later.

How Did We Manage the Implementation?

The actual migration process from Vanilla Redux to RTK took about three months, but it was a fairly seamless process. The RTK development team did a great job of making RTK reverse compatible with Vanilla Redux, meaning that Vanilla Reducers and RTK Slices can coexist in the same root reducer. If you haven’t used Redux before, a reducer is probably best understood as a mechanism that regulates how global application state is mutated. We took advantage of this feature and allocated a subset of the total reducers in the POS app to the Task Force team members who had the most experience in a given area of the app. One by one, we refactored a reducer from Vanilla to RTK, merged it into our main branch, and released it into production. This gradual approach allowed us to roll back if we released an RTK slice that broke something in production. Fewer refactored reducers meant a smaller surface area to roll back and debug.

In the majority of places we moved from the Vanilla immutable syntax to the easier to read mutable syntax provided by RTK. If you haven’t seen the Vanilla syntax migrated before, here's a simple example where we create a reducer that can add or update a message in the Redux State. The addMessage action creator dispatches an action to the reducer under the AddMessage case in the switch statement. The updateMessage action creator does something similar but just updates the message under the UpdateMessage section of the reducer switch statement.

The Equivalent RTK slice:

In this example, the addMessage and updateMessage mutations in the RTK slice are not only easier to read, but also more compact because the slice generates action creators for us. We transitioned to the mutable syntax in the majority of places in the app without any issue.

Our implementation differed from the ideal RTK implementation when it came to how we migrated our asynchronous logic. There’s a great feature of RTK called RTK query, which can be used for web requests, but this didn’t work because it doesn’t support GraphQL at this time.

The other option is to make web requests using createAsyncThunk, and using the pending and fulfilled actions that it generates. We used a modified version of this second option to achieve our migration. Moving wholesale to pending and fulfilled wasn’t something we ended up doing because it would have caused us to have to split our logic between the Thunk and the reducer. Although it’s more idiomatic to RTK, this below example shows how you would be forced to rewrite a lot of logic.

Our Hybrid approach to using Thunks:

The recommended builder method of using Thunks:

As you can see in the above snippets, our way of doing things kept the state contained inside reducer cases which needed to be dispatched independently. In the second approach, the extraReducers code is run automatically when a Thunk is completed successfully. We ended up using the ThunkAPI provided in createAsyncThunk for the time being. This was done to remove the risk of destabilizing the app during the migration. We left it to the teams to create separate projects to make better use of the generated actions and the builder as they see fit going forward.

What Did We Learn?

In our experience Redux Toolkit really lived up to all the hype. At the end of the migration we deleted 3,500 lines of code that had no purpose other than boilerplating. Developers involved were also really excited to go back to their home teams and build on top of our work.

We ran into a couple of gotchas along the way that are worth mentioning to help you in your refactoring journey.

First, it’s really useful to explicitly type your useDispatch, useSelector and useStore right away. Not doing this caused us to run into a lot of TypeScript warnings near the end of the project.

Second, if you await on your dispatch calls anywhere, you need to remember to use the unwrap method on them. Unlike Vanilla Redux, dispatch doesn’t return the value from dispatch. dispatch().unwrap() does what await dispatch() used to.

It’s probably important to mention here that there were some reducers that translated awkwardly into RTK. But this was often because, even in their Vanilla form, they weren’t following patterns recommended by the Redux team. We created subprojects to handle these situations on a case by case basis. If you have a lot of reducers that don’t follow traditional best practices for Redux, you may want to take care of those before starting the refactor.

RTK is a really easy way to reuse Vanilla Redux code and expertise while getting the benefits of a modern JavaScript state management solution. You can save a lot of code by migrating. Removing this code makes your state management architecture easier to understand and to debug. It also has the added benefit of making feature development faster because developers don’t need to write any boilerplate when making new features that require Redux. Our team loved working with it, and we’re excited to build more features on top of our new RTK base!

Dan is a Senior Software Engineer at Shopify working on the React Native Point of Sale Mobile App.

We all get shit done, ship fast, and learn. We operate on low process and high trust, and trade on impact. You have to care deeply about what you’re doing, and commit to continuously developing your craft, to keep pace here. If you’re seeking hypergrowth, can solve complex problems, and can thrive on change (and a bit of chaos), you’ve found the right place. Visit our Engineering career page to find your role.