How many times have you put your head down to work on a big project and -- in what feels like the blink of an eye -- several months or even years (ouch) pass by and you realize you haven't come up for air.
It can be difficult to keep current with the latest developments in the JavaScript (and React) ecosystem.
Well... answer me this, busy React developer:
- does writing boilerplate code make you afraid of carpal tunnel syndrome?
- does your heart skip a beat when you think of stateless functional components?
- have you been too busy to check out new React developments lately?
Stop what you're doing right now if you answered 'Yes' to all three questions!
You need React Hooks in your life.
Let me explain further.
I work on a couple of large React projects. One, in particular, predates create-react-app; it's big and old and fragile and -- honestly -- not done very well. In my defense, I was learning as I went along and it was built during a time when there simply were no Best Practices for React development.
For the past year or two I've wanted to update the codebase... fix flaws, clean it up, bring it up to date so I can leverage newer techniques, libraries, and language features, etc. But the task has been daunting to say the least.
Just recently, I finally bit the bullet (so to speak) and am going through the painful process of upgrading the infrastructure, which involves moving it to create-react-app, upgrading several libraries to latest (most notably: react-router), dropping Immutable.js, fixing new type errors, etc.
And trust me, it is painful -- but worth it for the prospect of being able to use React Hooks, which was the thing that pushed me over the edge.
React Hooks make React code so much easier to read and write. And I hope they also make React with TypeScript more approachable for beginners.
For comparison, here's an example of a modest React component, written as a stateless functional component (without hooks) in TypeScript:
import * as React from 'react';import { compose } from 'recompose';import { connect } from 'react-redux';import { withTranslation } from 'react-i18next';import { getBar } from './selectors';import { doSomething } from './actions';export type OwnProps = { foo: string;};const _SomeComponent = ({ foo, bar, onClick, t }: Props) => <div className={ foo } onClick={ onClick } > <span>{ t(bar) }</span> </div>;type StateProps = { bar: string | null;};type DispProps = { onClick: () => void;};const mapStateToProps = (state: State): StateProps => ({ foo: getFoo(state),});const mapDispatchToProps = (dispatch: Function): DispProps => ({ onClick: () => dispatch(doSomething()),});export type Props = OwnProps & StateProps & DispProps & WithTranslation;export const SomeComponent = compose<Props, OwnProps>( withTranslation(), connect<StateProps, DispProps, OwnProps>(mapStateToProps, mapDispatchToProps),)(_SomeComponent);
I've written that bittersweet pattern hundreds of times.
Writing React feels so good... yet, there is so much wrong with the code above. Specifically, it's unnecessarily complex; it's got bits of logic and type info strewn all over the file as if an explosion had scattered it all; and, if you look at the render tree, you'll see some nasty nesting -- especially for larger components that compose many more HOC's.
Now take a look at how elegant the same component is when we rewrite it with React hooks:
import * as React from 'react';import { useCallback } from 'react';import { useSelector, useDispatch } from 'react-redux';import { useTranslation } from 'react-i18next';import { getBar } from './selectors';import { doSomething } from './actions';export type Props = { foo: string;};export const SomeComponent = ({ foo }: Props) => { const { t } = useTranslation(); const bar = useSelector(getBar); const dispatch = useDispatch(); const onClick = useCallback(() => dispatch(doSomething()), [ dispatch ]); return ( <div className={ foo } onClick={ onClick } > <span>{ t(bar) }</span> </div> );};
How many differences can you spot?
Benefits
- eliminated mapStateToProps, mapDispatchToProps, and their type definitions
- eliminated some glue typing (combining the internal props types)
- the incoming props in SomeComponent are now strictly the props required by the caller (no pollution by internal props)
- moved state and dispatch logic into the component / more cohesive relationship between declaration and usage
- single-level node in the React render tree
- can export the component directly now instead of having a hacky internal component and a dressed-up public component
- the code is smaller by one third! (27 lines versus 40 lines)
- same level of type safety but with much less explicit typing required
- possibly eliminated need for recompose
- visually, it's cleaner and is nicer to look at
- the flow of code is more logical -- the output is at the bottom
Drawbacks
- the bound action is slightly more complex now (i.e. wrapping in useCallback, need to watch for dispatch changes)
Wrap up
I hope this was as much of an eye opener for you as it was for me.
I'm not rewriting all my legacy components right away... but definitely all new components from now on are going to use hooks and I'll rewrite old ones where it makes sense.
Learning More
Read more about hooks in the official React documentation: Introducing Hooks
Also check out my new video series where I show you how I build React components. (using React hooks starting with Episode 2 - Star Rating)