React Hooks
I started learning React Hooks this week, so lets go over that in this week’s post. Just to say it up front, I’ve really only been looking at these for a few days, so please don’t use this as a reference. That said, the flow isn’t that much different from React components, so I’m getting used to it pretty quickly. If you’re looking to read up on hooks, the documentation on the React website is quite reader friendly so I’d suggest to start there.
React hooks allow you to utilize equivalents of state and component lifecycle methods in functional components. In doing so, they create cleaner and more reusable code that is easier to understand. The two main hooks are useState
and useEffect
. In addition to these, I’ve also started working with useReducer
, a hook that allows you to set up something similar to a Redux store.
useState
useState
allows you to handle state in a functional component (something previously only available in class components). As with all hooks, they can be imported:
import React, { useState } from 'react'
or called from React:
import React from 'react'
React.useState('hi but from state')
useState
returns an array with a state variable and a method to update state. This return can be destructured and assigned as follows:
const [ state, setState ] = useState('hi but from state')
From here, the value of state (‘hi but from state’) can be referenced by the state variable, and can be reassigned by using the setState
method:
const [ state, setState ] = useState('hi but from state')
console.log(state) => 'hi but from state'
setState('goodbye but from state')
console.log(state) => 'goodbye but from state'
Note you can call these whatever you want:
const [ greeting, setGreeting ] = useState('hi but from state')
Unlike class components, the data stored in state does not have to be an object (we stored a string above, and numbers, arrays, objects are all ok). You can also create multiple state variables:
const [ greeting, setGreeting ] = useState('hi but from state')
const [ aNumber, setANumber ] = useState(1)
const [ anObject, setAnObject ] = useState({
greeting: 'hi but from state',
farewell: 'goodbye but from state'
})
Unlike class component this.setState
, useState
's setter function does not merge new and old state if using objects, so you’ll need to spread prevState:
const [ anObject, setAnObject ] = useState({
greeting: 'hi but from state',
farewell: 'goodbye but from state'
})setAnObject({ greeting: 'hello!'})
console.log(anObject)
=> { greeting: 'hello!' }setAnObject((prevState) => {
return {
...prevState,
farewell: 'goodbye but from state'
}
}
console.log(anObject)
=> { greeting: 'hello!', farewell: 'goodbye but from state' }
So yeah, long story short: useState
lets you use state in functional components!
useEffect
useEffect
allows you to use several component lifecycle methods in functional components. It can be thought of as a combination of componentDidMount
, componentDidUpdate
and componentWillUnmount
all wrapped up in one. useEffect
has two main functions, one that sets something up and another(potentially) that cleans up after. Unless opted out of(discussed later), the clean up and then setup functions will be run every time the component re-renders. I’m going to have trouble describing, so lets look at fake code:
const TotallyRealComponent = props => {
useEffect(() => {
subscribeToSomething()
return unsubscribeFromThatThing()
})
}
When TotallyRealComponent
mounts, subscribeToSomething
will run. From here, every time TotallyRealComponent
renders, unsubscribeFromThatThing
will run, followed by subscribeToSomething
. Finally, when the component unmounts, unsubscribeFromThatThing
will be called a final time.
This behavior can be opted out of in two ways utilizing useEffect
's second argument. If passed an array as the second argument, useEffect
will compare arrays on re-renders and run if there is a difference in result. Lets start with the first way: you can pass an empty array as the second argument. This will make it so that subscribeToSomething
runs once when the component mounts, and won’t run unsubscribeFromThatThing
until the component will unmount:
const TotallyRealComponent = props => {
useEffect(() => {
subscribeToSomething()
return unsubscribeFromThatThing()
}, []) // game changer here
}
The second way is to pass a variable or variables into the array as the second argument. Lets use the friendId example since we’re using Facebook’s framework:
const TotallyRealComponent = ({ friendId })=> {
useEffect(() => {
subscribeToSomething(friendId)
return unsubscribeFromThatThing(friendId)
}, [friendId]) // game changer here
}
If on re-render the friendId
is the same as the previous render, useEffect
will skip the clean up and mount functions. If they are different, unsubscribeFromThatThing
will run with the previous friendId
, and then subscribeToSomething
will run with the new friendId
. Finally, when the component unmounts, unsubscribeFromThatThing
will run with the last subscribed friendId
.
Last, since useEffect
is just a function, it can be called multiple times in the same component. This can be beneficial for separating areas of concern. Lets say you want to fetch something on mount, and incorporate the previous subscribe / unsubscribe functionality in a class component. Your componentDidMount
is starting to share functionality of separate concerns:
componentDidMount() {
fetch(someUrl)
.then(doSomething)
subscribeToSomething()
}componentDidUpdate() {
if (this.props.friendId !== prevProps.friendId) {
unsubscribeFromThatThing(prevProps.friendId)
subscribeToSomething(this.props.friendId)
}
}componentWillUnmount() {
unsubscribeFromThatThing(this.props.friendId)
}
While this example isn’t super noticable, if you imagine there being much more going on, you’ll start to have separate concerns grouped together, and similar concerns spread around your component. Lets look at this with hooks:
const TotallyRealComponent = ({ friendId })=> {
useEffect(() => {
fetch(someUrl)
.then(doSomething)
}, []) useEffect(() => {
subscribeToSomething(friendId)
return unsubscribeFromThatThing(friendId)
}, [friendId]) // game changer here
}
In this case, TotallyRealComponent
calls useEffect
twice, separating the initial fetch on component mount, and the ongoing management of the friend subscription. Much cleaner and easier to understand at a glance.
useReducer
Last, useReducer
! useReducer
allows you to, more or less, manage a more complex state using a reducer like you would with Redux. useReducer
returns a store and a dispatch function (sound familiar?), and accepts a reducer function and initial state:
const [ store, dispatch ] = useReducer(reducer, initialState)
Similar to useState
, the store
variable can be referenced to pull values from the store, and the dispatch function can be used to update the store. Dispatch and the store work nearly identically to Redux, so I’m not going to go too much into those here. The only real difference is that useReducer
is setting initial state — you don’t need to specify state having a default value in the reducer (you do need to define initialState
for useReducer
, though):
// don't do this
function reducer(state = {}, action) {// do this
function reducer(state, action) {
So yeah, that’s pretty much my understanding of the basics of hooks. So far I’ve been trying to implement these in all of my components, and I’m liking it quite a lot. I really like the separation of concerns that useEffect
brings, and useState
and useReducer
feel so much cleaner than state and Redux. Looking forward to utilizing them more!
Thanks for reading!