Editor’s note: This article was updated 12 May 2022 to include more up-to-date information on Redux.
As React picked up the pace in the frontend engineering arena, new patterns emerged to help make our applications more scalable and maintainable. With the introduction of global state management tools like Redux and MobX, some of these patterns extended their influence across the industry.
The human brain has a cognitive tendency to retain a limited number of things at a given instance. This leads us to divide our programs into smaller units so we can think, code, test, and fix one thing at a time, at times referred to as separation of concerns.
useSelector()
useDispatch()
With the introduction of Redux, a pattern for container and presentational components came forward in that Summer of 2015, when Dan Abramov wrote an amazing post about it.
The major concern of this pattern was to separate your business or global stateful logic from your presentational components. This makes it easier for developers to maintain focus only on relevant stuff at any given time; changes in one portion will not make any changes to another.
Thus, the developer writing or fixing the presentational layer has to make sure a11y standards and platform-based optimizations are delivering better aesthetics, while developers writing the business logic have to make sure the data delivered to the presentational component is derived properly out of the given props.
After the introduction of Hooks in React 16.8, things have changed a lot, as described by Dan in an update to the same blog:
Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times. The main reason I found it useful was because it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division.
But the core concept of separating business logic from presentational components can still make many of our complex problems easier to solve.
Since the announcement of Hooks, the React community has been very quick to adopt it. Likewise, React Redux has also added Hooks to their existing API. With a better developer experience and performance boosts, this API has brought some great improvements to codebases more inclined towards Hooks.
Being based on Hooks, your components are now free from the hassle of connected HOCs. In ducks patterns, our Redux store is divided in small chunks called “ducks”. Ducks work as suppliers of store for UI layer. For class components, the connect()
method was used to provide state and actions to ducks. These parts of the connect API are now available as two separate Hooks. Selection of state is now done using the useSelector
Hook, while the actions dispatcher is now available via the useDispatch
Hook.
useSelector()
This is nearly a conceptual replacement of mapStateToProps
(first argument) in the connect method. This Hook expects two functions as arguments: a selector function and an equality function. The selector will be called with the entire Redux store state as its only argument and has to return the relevant part of the state used by the component.
The equality function will be supplied with current and new state whenever the selector function is executed. If it returns a false value, the component will be forced to re-render; otherwise, the component will not re-render. By default, the equality function is a shallow comparison between two states.
useSelector()
After a lot of experience using Redux in a number of apps, and given the nature of Hooks-based APIs, the Redux team has made some wise changes to useSelector
in comparison to how mapStateToProps
works.
ownProps
argument is not available in this API since props are available with the functional component and can be used via closuresisEqual
or Immutable’s matcheruseDispatch()
The second argument to the connect method was a function that supplied action dispatchers to our components. After a thoughtful debate and a majority consensus of Redux’s Twitter community, Redux adopted useDispatch
over useActions
. It takes an action object as an argument that is then supplied to our reducer for changes in our global state.
Such transitions usually make developers a little cautious about their existing codebase. But since all of these new features gave applications a big boost in terms of performance and scalability, no one wants to miss out on them.
React is one of the best libraries when it comes to backward compatibility. As mentioned by the React team, class-based components are not going anywhere, and they will be supported by upcoming React versions for the foreseeable future.
But if you want to leverage some cool benefits that Hooks brought to the React ecosystem, then here is a guide to kick-start your transition.
Let’s consider an example of a container that fetches and provides a list of Hacker News items. We will explore how we can convert our containers to Hooks and keep them working in our existing codebase.
Our class-based container with children props implemented with class might look like this:
/* * * HackerNews * */ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; import { compose } from 'redux'; import { injectReducer, injectSaga } from "redux-injectors"; import reducer from './reducer'; import saga from './sagas'; import makeSelectHackerNews from './selectors'; import { fetch } from './actions'; class HackerNews extends React.PureComponent { componentDidMount() { const { hackerNews } = this.props; if (!hackerNews.data.length && !hackerNews.fetching) { this.props.fetch({ offset: 0, limit: 15, }); } } render() { const { fetching, data, error } = this.props.hackerNews; return this.props.children.call(null, { fetching, data, error, }); } } HackerNews.propTypes = { hackerNews: PropTypes.object.isRequired, children: PropTypes.func.isRequired, fetch: PropTypes.func.isRequired, }; const mapStateToProps = createStructuredSelector({ hackerNews: makeSelectHackerNews(), }); function mapDispatchToProps(dispatch) { return { fetch: (data) => dispatch(fetch(data)), }; } const withConnect = connect( mapStateToProps, mapDispatchToProps, ); const withReducer = injectReducer({ key: 'hackerNews', reducer }); const withSaga = injectSaga({ key: 'hackerNews', saga }); export default compose( withReducer, withSaga, withConnect, )(HackerNews);
After transformation, it will look like this:
/* * * HackerNews * */ import React from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { createStructuredSelector } from 'reselect'; import { useInjectReducer, useInjectSaga } from "redux-injectors"; import reducer from './reducer'; import saga from './sagas'; import makeSelectHackerNews from './selectors'; import { fetch } from './actions'; function useHackerNews(props) { useInjectReducer({ key: "hackerNews", reducer: reducer }); useInjectSaga({ key: "hackerNews", saga: saga }); const hackerNews = useSelector<>(makeSelectHackerNews, shallowEqual); const dispatch = useDispatch(); useEffect(() => { if (!hackerNews.data.length && !hackerNews.fetching) { dispatch(fetch({ offset: 0, limit: 15, })); } }, [hackerNews]); return hackerNews; } export default function HackerNews({ children, ...props }) { const hackerNews = useHackerNews(props); return children(hackerNews); };
As you can see, the code formerly placed outside our component class is now part of our functional component. We have moved the same selector method that was being used earlier to the new useSelector
Hook, and the same dispatch method is now available via the useDispatch
Hook.
Our new container provides us with an option to use it as a custom Hook for our new functional component and keeps the props-based child components running as smoothly as they were earlier.
One of the major benefits functional components have over class-based components is fewer lines of code. This gives you slightly better performance as compared to class-based components, and it could make a significant difference in large-scale apps.
Hooks also make our components more readable by grouping connected logic together. Here in our container, we don’t have to scroll down to understand mapStateToProps
or mapDispatchToProps
. Besides this, we got rid of connected HOCs, which will decrease the number of nodes in our component hierarchy.
Hooks are now the recommended approach by Redux, as the official doc mentions:
We recommend using the React-Redux hooks API as the default approach in your React components. The existing connect API still works and will continue to be supported, but the Hooks API is simpler and works better with TypeScript.
Following the announcement of Hooks and Context in React, there was a lot of debate on whether we need Redux anymore — is it becoming obsolete?
IMHO, this question can have different answers depending upon the use case. Redux still serves the purpose it was made for and is one of the most reliable state management libraries for large-scale applications.
When I first developed with React, it was without any global state management and just used local state for everything. As our application grew bigger, we realized the need for global state, and since then, it has been quite an awesome experience working with Redux.
We adopted this container presentation pattern for our frontend at Peekaboo Guru, and we don’t have any regrets to date; we’re celebrating the third anniversary of our product.
Besides this, React has one of the most attractive taglines: “Learn once, write everywhere.” With as much effort as Facebook has been putting into React Native and other React renderers, it’s now easier to leverage not only your learnings, but also your codebase across platforms. If implemented in a real manner, this pattern allows you to share a lot of code across your React apps for different platforms.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]