Reselect 5.0 and Redux Thunk 3.0: What's New?

Anton Ioffe - January 8th 2024 - 10 minutes read

In the ever-evolving world of JavaScript development, Redux Toolkit and Reselect have long stood as pillars for robust state management. With the arrival of Redux Toolkit 2.0 and Reselect 5.0, the landscape is set to shift once more, bringing forth a suite of innovations aimed at refining your development experience. As we peel back the layers of these significant updates, from API enhancements to toolchain refinements, and navigate the tide of breaking changes, prepare to dive deep into the newest features that promise to reshape how we manage state and handle asynchrony with finesse. Join us as we explore not only the present breakthroughs but also cast an anticipatory glance toward the roadmap that paints the future of Redux and Reselect in the bustling ecosystem of modern web development.

Redux Toolkit 2.0 and Reselect 5.0: Innovations and Breakthroughs

Redux Toolkit 2.0 has revitalized state management with key enhancements for more modular and maintainable applications. A crucial development is the streamlined approach to reducer code-splitting. Redux Toolkit has built upon the foundation of combineReducers with utilities that facilitate the injection of reducers on demand. This improved approach ensures that applications can dynamically load the state logic they require when they require it, minimizing initialization bloat and taking code-splitting to a more granular level. This eliminates a great deal of the previously necessary boilerplate, resulting in a more maintainable codebase.

Let's look at a code example where injectReducer is used to dynamically load a slice reducer when a route is accessed in a React application:

import { injectReducer } from '@reduxjs/toolkit';
import { usersReducer } from './features/usersSlice';

// Assume we have a setup where the store and dynamic route components are already configured
function handleRouteChange(route) {
    if (route === '/users') {
        injectReducer('users', usersReducer);
    }
}

The createSlice function, a staple of Redux Toolkit, continues to simplify the creation of both action creators and reducers. Plus, the ongoing enhancements have made constructing selectors within created slices more intuitive. Now, coupling actions with the state slice they manage is more direct, and when utilized judiciously, this tightens the relationship between state changes and their triggers.

In tandem, Reselect 5.0 has expanded how developers can leverage createSelector with its new options for memoization. By passing an options object, developers can swap out the default memoization function for alternative memoizers or entirely different libraries tailored to specific performance requirements. This flexibility is vital for complex applications where fine-tuning selector performance can yield significant render optimizations.

Here's how you might use an alternative memoization strategy with createSelector:

import { createSelectorCreator, defaultMemoize } from 'reselect';
import isEqual from 'lodash.isequal';

const createSelectorWithDeepEqual = createSelectorCreator(
    defaultMemoize,
    isEqual
);

// Usage of the custom createSelector using deep equality checks
const selectUserById = createSelectorWithDeepEqual(
    [(state, userId) => state.users.byId[userId]],
    (user) => user
);

Reselect 5.0's advancements are further amplified through its improved synergy with Redux Toolkit's entityAdapter. When used in the combination, it enables seamless selector constructions that effortlessly integrate with normalized state architectures. This harmonious integration underscores the thoughtful design considerations in the evolution of both libraries.

In this updated landscape, Redux Toolkit 2.0 and Reselect 5.0 urge a revitalization of practices in managing complex application states. With Redux Toolkit's advances in dynamic code-splitting and the empowered createSlice selectors, state logic becomes fluid and coherent. Concurrently, Reselect's improvements in selector customizability underscore a commitment to performance. Collectively, these updates herald a new era in state management that supports and grows with the needs of contemporary web development.

Embracing the Modern JavaScript Ecosystem: ESM and Toolchain Refinements

The evolution toward ES Modules (ESM) represents a significant paradigm shift in the JavaScript ecosystem, reflecting an industry-wide progression toward standardization and modernization. Redux-related libraries have not been immune to this trend. One substantial step in this direction was the addition of the "exports" field in the package.json file. This strategic move streamlines package consumption by specifying entry points for different consumption scenarios, such as requiring a module via Node or a web application. The primary benefit is a more predictable import mechanism for consumers, enabling tools to enforce encapsulation and optimize dependency resolution. However, developers need to be aware of the potential impact on their build configurations which must now accommodate this field to leverage these benefits.

A tangible demonstration of these shifts is evident in the Redux Thunk 3.0 update. Previously, the Redux Thunk package used a combination of Babel and Rollup for builds. In maintaining pace with modern toolchain improvements, it now employs ESBuild—an increasingly popular, performance-optimized bundler. This transition illustrates a broader trend: the contemporary JavaScript development landscape favors leaner, faster tools that reduce build times and simplify configurations. While this switch promises quicker iteration cycles for development, it also means that developers may need to familiarize themselves with new tools and possibly reconfigure their continuous integration pipelines to benefit fully from the efficiency gains.

To align with modern development practices, Redux has transitioned away from producing UMD build artifacts. With the decreasing prevalence of no-bundler environments, Redux packages now provide a production-mode, browser-ready ESM build artifact at dist/$PACKAGENAME.browser.mjs, optimized for modern browser use. Despite dropping UMD support, this alternative empowers developers to embrace more current syntax without process.env.NODEENV references, supporting script tag imports with ESM support from CDNs like Unpkg. This move suits teams focusing on modern browser targets, while those still supporting older platforms must now seek other solutions to maintain compatibility, such as robust polyfills.

Central to this modernization push is the decision not to transpile build outputs. Directly targeting modern JavaScript (ES2020) harnesses the latest language features, promoting code cleanliness and module conciseness. Such an approach brings potential performance improvements in up-to-date environments but introduces challenges in legacy browser support. Developers are now obligated to implement their transpilation steps for backward compatibility, adding complexity to build systems and potentially lengthening build times for projects that require broad browser support.

Delving deeper into Redux Thunk, the shift towards named exports over a default export signifies an alignment with the ESM philosophy of explicit, named bindings. By exposing thunk and withExtraArgument as named exports, the core Redux Thunk functionality becomes more apparent and tree-shaking optimized. While the existing codebases using Redux Toolkit will see no effect here, projects directly relying on Redux Thunk must undergo refactoring to utilize these named exports. This breaking change further encourages the practice of explicit module usage, increasing readability and maintainability for scalable applications while necessitating careful update strategies for developers managing upgrades in large-scale applications.

The deprecation of createStore signals Redux's strategic move towards the configureStore method supplied by Redux Toolkit, guiding developers to a modern and feature-rich setup. While createStore will not be removed and is still operational, it now comes with a visual strikethrough, implying it is outdated. Developers can suppress this visual indication by replacing createStore with legacy_createStore, ensuring a seamless upgrade for existing systems.

// Before
import { createStore } from 'redux';
const store = createStore(reducer);

// After
// Option 1: Switch to Redux Toolkit's configureStore
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({ reducer });

// Option 2: Use legacy_createStore to avoid deprecation indicators
import { legacy_createStore as createStore } from 'redux';
const store = createStore(reducer);

The shift to TypeScript within the Redux core library introduces type safety benefits. However, this move comes with the necessity to reconcile possible type discrepancies after upgrading to Redux core v5. Teams may need to examine their type definitions and adjust accordingly to fit the TypeScript implementation, advocating for improved maintainability and reduced errors.

The requirement for action types to be strings, contrary to previous versions where Symbols could occasionally be utilized, underscores Redux's doctrine of action serializability. The transition might require minimal code changes, given the rarity of non-string action types, but it ensures conformance to Redux's best practices.

// Before (uncommon and non-standard usage)
const MY_ACTION = Symbol('MY_ACTION');
const actionCreator = () => ({ type: MY_ACTION });

// After
const MY_ACTION = 'MY_ACTION';
const actionCreator = () => ({ type: MY_ACTION });

The update to Redux middleware introduces changes that promote type uncertainty, demanding alterations to existing middleware to accord with the stricter typescript typing. Following these updates can help enhance confidence in the predictability and consistency of the application's state changes.

// Before
const myMiddleware = ({ dispatch, getState }) => next => action => {
    // Middleware logic
    return next(action);
};

// After (using TypeScript for improved type safety)
import { Middleware, Dispatch, AnyAction } from 'redux';

const myMiddleware: Middleware<{}, any, Dispatch<AnyAction>> = store => next => (action: AnyAction) => {
    // Middleware logic here, with action typed based on AnyAction
    return next(action);
};

In light of these changes, developers are encouraged to deliberate on how TypeScript's compile-time type system can enhance the robustness of their applications and identify where middleware can accommodate these types within the codebase. Embracing these updates is imperative to stay aligned with the evolution of Redux.

Redux Thunk 3.0: Reinventing Middlewares and Asynchrony

Redux Thunk 3.0 introduces a fresh perspective on handling asynchronous logic in Redux by emphasizing dynamic middlewares and autoBatchEnhancer inclusion. The ability to add and remove middleware dynamically, potentially useful for scenarios like code splitting, adds a layer of flexibility that was previously harder to achieve. A notable feature is the createDynamicMiddleware utility, which allows developers to enhance their store configuration with middlewares that can be injected or ejected at runtime, based on the evolving needs of the application.

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit';

const dynamicMiddleware = createDynamicMiddleware();
const store = configureStore({
    reducer: { todos: todosReducer },
    middleware: getDefaultMiddleware => getDefaultMiddleware().prepend(dynamicMiddleware.middleware)
});

// Later in the application lifecycle
dynamicMiddleware.addMiddleware(someOtherMiddleware);

This flexibility in middleware management is particularly beneficial in large-scale applications where features may need to be toggled or loaded on-demand, aiding in the reduction of initial payload sizes and optimizing resource utilization.

The discussion around performance would be incomplete without mentioning the default inclusion of autoBatchEnhancer in Redux Thunk 3.0's store configuration. This enhancer aggregates multiple "low-priority" dispatched actions into a single batch, mitigating the performance hit from triggering numerous consecutive UI updates. It exemplifies the commitment to not just functional scalability but also to ensuring that the responsiveness and speed of Redux applications are maintained, even as complexity grows.

One might wonder how this impacts the predictability of the application state. Redux has always thrived on the predictability and traceability of state changes, and by introducing controlled batching, we are enhancing performance without sacrificing these foundational principles. To fully leverage this, one must understand the nature of their actions and design their state management strategies around the prioritization of action dispatches.

However, with new features come new responsibilities. Developers must now be more deliberate in managing the middleware lifecycle, maintaining awareness of the middleware stack to avoid potential clashes or performance bottlenecks due to inadvertently added middlewares. Questions arise, such as, "What strategies should we adopt to ensure that dynamically added middleware does not interfere with existing application logic?" and "How can we effectively track and debug the state changes introduced by these new capabilities?"

Code robustness and maintainability are also taken into account with Redux Thunk 3.0's shift away from a single default export towards named exports. This move favors explicitness and can result in better tree-shaking, ultimately leading to optimized bundles. With the middleware now clearly delineated through named exports (thunk and withExtraArgument), developers can more easily reason about their imports and maintain tight control over their store's capabilities. Potential coding mistakes, such as mistakenly importing the entire module rather than specific functionalities, are mitigated, leading to a more deliberate and error-resistant codebase.

import { thunk, withExtraArgument } from 'redux-thunk';

// Use thunk as the middleware
// withExtraArgument can be used to inject custom arguments into thunks

In conclusion, Redux Thunk 3.0 embarks on a journey to refine asynchronous control flow, inviting applications to approach side effects with both nuance and rigor. There's a palpable excitement in adopting these advancements; yet, it’s tempered by the need for thorough understanding and deliberate integration into the existing architecture. Are we prepared to navigate the complexities introduced by these features, and how will we measure the true impact on performance and maintainability in our real-world applications?

Forward Looking: Roadmap and Future Enhancements

As we anticipate developments in state management, the introduction of custom slice reducer creators in Redux demonstrates a commitment to more configurable and adaptive management of state slices. These tools will enable developers to sculpt state logic with precision, ensuring it aligns tightly with the demands of the application. However, this newfound flexibility raises questions about striking an optimal balance between the reusability of state logic and the adherence to a modular and cleanly architected system. Developers should consider the impact of custom state logic on the principles of modularity: Will this lead to enhanced productivity or might it generate complex interdependencies within large-scale applications?

With RTK Query 3.0 on the horizon, we expect to see refinements such as more sophisticated caching and query invalidation methods. These improvements are likely to significantly enhance application performance, reducing unnecessary network traffic and preserving data integrity. Developers should ponder the ramifications: How could enhanced caching techniques shape application architectures, and how might they be harnessed to build even more reactive and resource-efficient experiences for users?

The Redux ecosystem is poised for enhancement, and we must consider how this could recalibrate the distribution of responsibilities between Redux and client applications. The logical integration of data fetching with state management might prompt concern over a shift towards monolithic structures; yet, it also holds the promise of a more unified and manageable codebase. The challenge for developers is to adapt to these changes judiciously, guarding against premature optimization while readying themselves for a potentially more integrated approach to managing state and data.

Anticipated modifications also include the introduction of an abstraction layer for CRUD operations via RTK Query's automated query generation and sophisticated entity handling with tools like createEntityAdapter. As these approaches threaten to abstract state operations, it is critical for developers to weigh the benefits of streamlining against the potential loss of finely-grained control. Will these abstractions amplify developer productivity without compromising the capacity for finely-tuned application logic?

Finally, we encourage developers to reflect on the potential impact of these evolutions on the larger JavaScript landscape. With state management practices undergoing refinement, we ought to consider the downstream effects on various frameworks and libraries. The interplay between state management techniques and UI frameworks suggests that changes in Redux and Reselect could catalyze innovation beyond their immediate ecosystem. As Redux pursues these forthcoming enhancements, the JavaScript community is positioned at the forefront of a wave of updates that could redefine how we approach state management in the modern web development era.

Summary

The article explores the new features and updates in Redux Toolkit 2.0 and Reselect 5.0, emphasizing their impact on state management and handling asynchrony in modern JavaScript web development. Key takeaways include the streamlined approach to reducer code-splitting, enhanced selector customizability, improved synergy between Redux Toolkit and Reselect, and the shift towards ES Modules. The article challenges developers to consider the implications of these updates on their existing systems and provides a task of refactoring middleware to utilize named exports in Redux Thunk 3.0. By embracing these advancements, developers can stay aligned with the evolving landscape of state management in contemporary web development.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers