Introduction
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.
Changing patterns for SoCs
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.
Redux Hooks: A jump on the bandwagon
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 the container component pattern, our containers are the components connected to the Redux store using the connect()
method. These containers get some part of global state as props (optionally using reselect to get a relevant chunk out of global state) and a dispatch method to initiate changes in global state.
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.
What’s new in 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.
- The selector function may return any value, not just the object
- The
ownProps
argument is not available in this API since props are available with the functional component and can be used via closures - The equality function can be modified: it can be Lodash’s
isEqual
or Immutable’s matcher
useDispatch()
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.
Containers and Hooks: A smooth transformation
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 from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
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 PropTypes from 'prop-types';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import reducer from './reducer';
import saga from './sagas';
import makeSelectHackerNews from './selectors';
import { fetch } from './actions';
function useHackerNews(props) {
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);
};
HackerNews.propTypes = {
children: PropTypes.func.isRequired,
};
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.
What are the upsides?
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 connect HOCs, which will decrease the number of nodes in our component hierarchy.
Is Redux here to stay?
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.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post How to convert your existing Redux containers to Hooks appeared first on LogRocket Blog.
Top comments (0)