The first thing you should do whenever you're about to learn something new is ask yourself two questions -
-
Why does this thing exist?
-
What problems does this thing solve?
If you never develop a convincing answer for both of those questions, you won't have a solid enough foundation to build upon when you dive into the specifics. These questions are specifically interesting in regards to React Hooks. React was the most popular and most loved front-end framework in the JavaScript ecosystem when Hooks were released. Despite the existing praise, the React team still saw it necessary to build and release Hooks. Lost in the various Medium posts and blog think pieces on Hooks are the reasons (1) why and for what (2) benefit, despite high praise and popularity, the React team decided to spend valuable resources building and releasing Hooks. To better understand the answers to both of these questions, we first need to take a deeper look into how we've historically written React apps.
createClass
If you've been around the React game long enough, you'll remember the React.createClass
API. It was the original way in which we'd create React components. All of the information you'd use to describe the component would be passed as an object to createClass
.
const ReposGrid = React.createClass({getInitialState () {return {repos: [],loading: true}},componentDidMount () {this.updateRepos(this.props.id)},componentDidUpdate (prevProps) {if (prevProps.id !== this.props.id) {this.updateRepos(this.props.id)}},updateRepos (id) {this.setState({ loading: true })fetchRepos(id).then((repos) => this.setState({repos,loading: false}))},render() {const { loading, repos } = this.stateif (loading === true) {return <Loading />}return (<ul>{repos.map(({ name, handle, stars, url }) => (<li key={name}><ul><li><a href={url}>{name}</a></li><li>@{handle}</li><li>{stars} stars</li></ul></li>))}</ul>)}})
createClass
was a simple and effective way to create React components. The reason React initially used the createClass
API was because, at the time, JavaScript didn't have a built-in class system. Of course, this eventually changed. With ES6, JavaScript introduced the class
keyword and with it a native way to create classes in JavaScript. This put React in a tough position. Either continue using createClass
and fight against the progression of JavaScript or submit to the will of the EcmaScript standard and embrace classes. As history has shown, they chose the latter.
React.Component
We figured that we’re not in the business of designing a class system. We just want to use whatever is the idiomatic JavaScript way of creating classes. - React v0.13.0 Release
React v0.13.0 introduced the React.Component
API which allowed you to create React components from (now) native JavaScript classes. This was a big win as it better aligned React with the EcmaScript standard.
class ReposGrid extends React.Component {constructor (props) {super(props)this.state = {repos: [],loading: true}this.updateRepos = this.updateRepos.bind(this)}componentDidMount () {this.updateRepos(this.props.id)}componentDidUpdate (prevProps) {if (prevProps.id !== this.props.id) {this.updateRepos(this.props.id)}}updateRepos (id) {this.setState({ loading: true })fetchRepos(id).then((repos) => this.setState({repos,loading: false}))}render() {if (this.state.loading === true) {return <Loading />}return (<ul>{this.state.repos.map(({ name, handle, stars, url }) => (<li key={name}><ul><li><a href={url}>{name}</a></li><li>@{handle}</li><li>{stars} stars</li></ul></li>))}</ul>)}}
Though a clear step in the right direction, React.Component
wasn't without its trade-offs.
constructor
With Class components, you initialize the state of the component inside of the constructor
method as a state
property on the instance (this
). However, according to the ECMAScript spec, if you're extending a subclass (in this case, React.Component
), you must first invoke super
before you can use this
. Specifically, when using React, you also have to remember to pass props
to super
.
constructor (props) {super(props) // 🤮...}
Autobinding
When using createClass
, React would auto-magically bind all the methods to the component's instance, this
. With React.Component
, that wasn't the case. Very quickly, React developers everywhere realized they didn't know how the this keyword worked. Instead of having method invocations that "just worked", you had to remember to .bind
methods in the class's constructor
. If you didn't, you'd get the popular "Cannot read property setState
of undefined" error.
constructor (props) {...this.updateRepos = this.updateRepos.bind(this) // 😭}
Now I know what you might be thinking. First, these issues are pretty superficial. Sure calling super(props)
and remembering to bind
your methods is annoying, but there's nothing fundamentally wrong here. Second, these aren't necessarily even issues with React as much as they are with the way JavaScript classes were designed. Both points are valid. However, we're developers. Even the most superficial issues become a nuisance when you're dealing with them 20+ times a day. Luckily for us, shortly after the switch from createClass
to React.Component
, the Class Fields proposal was created.
Class Fields
Class fields allow you to add instance properties directly as a property on a class without having to use constructor
. What that means for us is that with Class Fields, both of our "superficial" issues we previously talked about would be solved. We no longer need to use constructor
to set the initial state of the component and we no longer need to .bind
in the constructor
since we could use arrow functions for our methods.
class ReposGrid extends React.Component {state = {repos: [],loading: true}componentDidMount () {this.updateRepos(this.props.id)}componentDidUpdate (prevProps) {if (prevProps.id !== this.props.id) {this.updateRepos(this.props.id)}}updateRepos = (id) => {this.setState({ loading: true })fetchRepos(id).then((repos) => this.setState({repos,loading: false}))}render() {const { loading, repos } = this.stateif (loading === true) {return <Loading />}return (<ul>{repos.map(({ name, handle, stars, url }) => (<li key={name}><ul><li><a href={url}>{name}</a></li><li>@{handle}</li><li>{stars} stars</li></ul></li>))}</ul>)}}
So now we're good, right? Unfortunately, no. The move from createClass
to React.Component
came with some tradeoffs, but as we saw, Class Fields took care of those. Unfortunately, there are still some more profound (but less talked about) issues that exist with all the previous versions we've seen.
The whole idea of React is that you're better able to manage the complexity of your application by breaking it down into separate components that you then can compose together. This component model is what makes React so elegant. It's what makes React, React. The problem, however, doesn't lie in the component model, but in how the component model is implemented.
Duplicate Logic
Historically, how we've structured our React components has been coupled to the component's lifecycle. This divide naturally forces us to sprinkle related logic throughout the component. We can clearly see this in the ReposGrid
example we've been using. We need three separate methods (componentDidMount
, componentDidUpdate
, and updateRepos
) to accomplish the same thing - keep repos
in sync with whatever props.id
is.
componentDidMount () {this.updateRepos(this.props.id)}componentDidUpdate (prevProps) {if (prevProps.id !== this.props.id) {this.updateRepos(this.props.id)}}updateRepos = (id) => {this.setState({ loading: true })fetchRepos(id).then((repos) => this.setState({repos,loading: false}))}
To fix this, we'd need a whole new paradigm for the way in which we'd handle side effects in React components.
Sharing Non-visual Logic
When you think about composition in React, odds are you think in terms of UI composition. This is natural since it's what React is so good at.
view = fn(state)
Realistically, there's more to building an app than just the UI layer. It's not uncommon to need to compose and reuse non-visual logic. However, because React couples UI to a component, this can be difficult. Historically, React hasn't had a great answer for this.
Sticking with our example, say we needed to create another component that also needed the repos
state. Right now, that state and the logic for handling it lives inside of the ReposGrid
component. How would we approach this? Well, the simplest approach would be to copy all of the logic for fetching and handling our repos
and paste it into the new component. Tempting, but nah. A smarter approach would be to create a Higher-Order Component that encapsulated all of the shared logic and passed loading
and repos
as props to whatever component needed it.
function withRepos (Component) {return class WithRepos extends React.Component {state = {repos: [],loading: true}componentDidMount () {this.updateRepos(this.props.id)}componentDidUpdate (prevProps) {if (prevProps.id !== this.props.id) {this.updateRepos(this.props.id)}}updateRepos = (id) => {this.setState({ loading: true })fetchRepos(id).then((repos) => this.setState({repos,loading: false}))}render () {return (<Component{...this.props}{...this.state}/>)}}}
Now whenever any component in our app needed repos
(or loading
), we could wrap it in our withRepos
HOC.
// Profile.jsfunction Profile ({ loading, repos }) {...}export default withRepos(Profile)
This works and historically (along with Render Props) has been the recommended solution for sharing non-visual logic. However, both these patterns have some downsides.
First, if you're not familiar with them (and even when you are), your brain can get a little wonky following the logic. With our withRepos
HOC, we have a function that takes the eventually rendered component as the first argument but returns a new class component which is where our logic lives. What a convoluted process.
Next, what if we had more than one HOC we were consuming. As you can imagine, it gets out of hand pretty quickly.
export default withHover(withTheme(withAuth(withRepos(Profile))))
Worse than ^ is what eventually gets rendered. HOCs (and similar patterns) force you to restructure and wrap your components. This can eventually lead to "wrapper hell" which again, makes it harder to follow.
<WithHover><WithTheme hovering={false}><WithAuth hovering={false} theme='dark'><WithRepos hovering={false} theme='dark' authed={true}><Profileid='JavaScript'loading={true}repos={[]}authed={true}theme='dark'hovering={false}/></WithRepos></WithAuth><WithTheme></WithHover>
Current State
So here's where we're at.
- React is hella popular.
- We use Classes for React components cause that's what made the most sense at the time.
- Calling super(props) is annoying.
- No one knows how "this" works.
- OK, calm down. I know YOU know how "this" works, but it's an unnecessary hurdle for some.
- Organizing our components by lifecycle methods forces us to sprinkle related logic throughout our components.
- React has no good primitive for sharing non-visual logic.
Now we need a new component API that solves all of those problems while remaining simple, composable, flexible, and extendable. Quite the task, but somehow the React team pulled it off.
React Hooks
Since React v0.14.0, we've had two ways to create components - classes or functions. The difference was that if our component had state or needed to utilize a lifecycle method, we had to use a class. Otherwise, if it just accepted props and rendered some UI, we could use a function.
Now, what if this wasn't the case. What if instead of ever having to use a class, we could just always use a function.
Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.
- John Carmack. Oculus VR CTO.
Sure we'd need to figure out a way to add the ability for functional components to have state and lifecycle methods, but assuming we did that, what benefits would we see?
Well, we would no longer have to call super(props)
, we'd no longer need to worry about bind
ing our methods or the this
keyword, and we'd no longer have a use for Class Fields. Essentially, all of the "superficial" issues we talked about earlier would go away.
(ノಥ,_」ಥ)ノ彡 React.Component 🗑function ヾ(Ő‿Ő✿)
Now, the harder issues.
- State
- Lifecycle methods
- Sharing non-visual logic
State
Since we're no longer using classes or this
, we need a new way to add and manage state inside of our components. As of React v16.8.0, React gives us this new way via the useState
method.
useState
is the first of many "Hooks" you'll be seeing in this course. Let the rest of this post serve as a soft introduction. We'll be diving much deeper intouseState
as well as other Hooks in future sections.
useState
takes in a single argument, the initial value for the state. What it returns is an array with the first item being the piece of state and the second item being a function to update that state.
const loadingTuple = React.useState(true)const loading = loadingTuple[0]const setLoading = loadingTuple[1]...loading // truesetLoading(false)loading // false
As you can see, grabbing each item in the array individually isn't the best developer experience. This is just to demonstrate how useState
returns an array. Typically, you'd use Array Destructuring to grab the values in one line.
// const loadingTuple = React.useState(true)// const loading = loadingTuple[0]// const setLoading = loadingTuple[1]const [ loading, setLoading ] = React.useState(true) // 👌
Now let's update our ReposGrid
component with our new found knowledge of the useState
Hook.
function ReposGrid ({ id }) {const [ repos, setRepos ] = React.useState([])const [ loading, setLoading ] = React.useState(true)if (loading === true) {return <Loading />}return (<ul>{repos.map(({ name, handle, stars, url }) => (<li key={name}><ul><li><a href={url}>{name}</a></li><li>@{handle}</li><li>{stars} stars</li></ul></li>))}</ul>)}
- State ✅
- Lifecycle methods
- Sharing non-visual logic
Lifecycle methods
Here's something that may make you sad (or happy?). When using React Hooks, I want you to take everything you know about the traditional React lifecycle methods as well as that way of thinking, and forget it. We've already seen the problem of thinking in terms of the lifecycle of a component - "This [lifecycle] divide naturally forces us to sprinkle related logic throughout the component." Instead, think in terms of synchronization.
Think of any time you've ever used a lifecycle event. Whether it was to set the initial state of the component, fetch data, update the DOM, anything - the end goal was always synchronization. Typically, synchronizing something outside of React land (an API request, the DOM, etc.) with something inside of React land (component state) or vice versa.
When we think in terms of synchronization instead of lifecycle events, it allows us to group together related pieces of logic. To do this, React gives us another Hook called useEffect
.
Defined, useEffect
lets you perform side effects in function components. It takes two arguments, a function, and an optional array. The function defines which side effects to run and the (optional) array defines when to "re-sync" (or re-run) the effect.
React.useEffect(() => {document.title = `Hello, ${username}`}, [username])
In the code above, the function passed to useEffect
will run whenever username
changes. Therefore, syncing the document's title with whatever Hello, ${username}
resolves to.
Now, how can we use the useEffect
Hook inside of our code to sync repos
with our fetchRepos
API request?
function ReposGrid ({ id }) {const [ repos, setRepos ] = React.useState([])const [ loading, setLoading ] = React.useState(true)React.useEffect(() => {setLoading(true)fetchRepos(id).then((repos) => {setRepos(repos)setLoading(false)})}, [id])if (loading === true) {return <Loading />}return (<ul>{repos.map(({ name, handle, stars, url }) => (<li key={name}><ul><li><a href={url}>{name}</a></li><li>@{handle}</li><li>{stars} stars</li></ul></li>))}</ul>)}
Pretty slick, right? We've successfully gotten rid of React.Component
, constructor
, super
, this
and more importantly, we no longer have our effect logic sprinkled (and duplicated) throughout the component.
- State ✅
- Lifecycle methods ✅
- Sharing non-visual logic
Sharing non-visual logic
Earlier we mentioned that the reason React didn't have a great answer to sharing non-visual logic was because "React couples UI to a component". This lead to overcomplicated patterns like Higher-order components or Render props. As you can probably guess by now, Hooks have an answer for this too. However, it's probably not what you think. There's no built-in Hook for sharing non-visual logic, instead, you can create your own custom Hooks that are decoupled from any UI.
We can see this in action by creating our own custom useRepos
Hook. This Hook will take in an id
of the Repos we want to fetch and (to stick to a similar API) will return an array with the first item being the loading
state and the second item being the repos
state.
function useRepos (id) {const [ repos, setRepos ] = React.useState([])const [ loading, setLoading ] = React.useState(true)React.useEffect(() => {setLoading(true)fetchRepos(id).then((repos) => {setRepos(repos)setLoading(false)})}, [id])return [ loading, repos ]}
What's nice is any logic that's related to fetching our repos
can be abstracted inside of this custom Hook. Now, regardless of which component we're in and even though it's non-visual logic, whenever we need data regarding repos
, we can consume our useRepos
custom Hook.
function Profile ({ user }) {const [ loading, repos ] = useRepos(user.id)...}
- State ✅
- Lifecycle methods ✅
- Sharing non-visual logic ✅
The marketing pitch for Hooks is that you're able to use state inside function components. In reality, Hooks are much more than that. They're about improved code reuse, composition, and better defaults. There's a lot more to Hooks we still need to cover, but now that you know WHY they exist, we have a solid foundation to build on.