On February 6 React 16.8 was released and with React 16.8, React Hooks are available in a stable release!
That means we can start using Hooks without the fear of writing unstable code.
Here is a live example of the code in this post:
https://frontarm.com/demoboard/?id=af4f455f-1d30-4823-90a9-b15cfc3e92f9
But, what are Hooks?
Hooks are functions that allow you to use state and “life cycle” for functional Components, that means you don’t need to write classes in order to build React components.
For me, this is exciting because I don’t like to bind “this” (you saw what I did there?). And what I mean is, that Classes in React are kind of messy (I'm not the only one who think that). Most of the time it has something to do with “this”. But that is a story for another time.
So you get access almost all the state lifecycle without using classes. But how?
Which Hooks are available and what they do?
Directly from React’s page, we have this list of Hooks (https://reactjs.org/docs/hooks-reference.html)
I will try to start with the first 3 elements of that list. (at the beginning I was thinking in 4 but the post got too long, I will split it)
useState Hook
This hook lets you have an internal state inside a functional Component. Here is a small comparison side by side of a counter component written as Class Component and as Function Component with useState.
Let's use a counter as an example:
import React from 'react'
export default class Counter extends React.Component {
constructor (props) {
super(props)
this.state = {
count: props.initialCount || 0
}
this.add = this.add.bind(this)
this.minus = this.minus.bind(this)
this.reset = this.reset.bind(this)
}
add () {
this.setState((state) => ({ count: state.count + 1 }))
}
minus () {
this.setState((state) => ({ count: state.count - 1 }))
}
reset () {
this.setState((state) => ({ count: 0 }))
}
render () {
return (
<div className="counter">
<span className="counter\_\_count">{ this.state.count }</span>
<button onClick={this.add}>+</button>
<button onClick={this.minus}>-</button>
<button onClick={this.reset}>Clear</button>
</div>
)
}
}
Now the same but with useState
import React, { useState, useEffect } from 'react'
export default function CounterUseState (props) {
const [count, setCount] = useState(props.initialCount || 0)
const add = () => { setCount(count + 1) }
const minus = () => { setCount(count - 1) }
const reset = () => { setCount(0) }
return (
<div className="counter">
<span className="counter\_\_count">{ count }</span>
<button onClick={add}>+</button>
<button onClick={minus}>-</button>
<button onClick={reset}>Clear</button>
</div>
)
}
We don’t have more “this” anymore! as result the “state” is easier to use, the handlers are cleaner and it would be easier to compose with functions outside of the component scope.
Now how we can handle with the “life cycle” of the component? we will use the useEffect Hook
useEffect Hook
This Hook received two parameters, the first argument is a callback function to run when the component is rendered, the callback will be called either is the first time the component is rendered ( componentDidMount ) or every time it’s re-rendered ( componentDidUpdate ), the second parameter is very important for this because with determines what variables or props you want to “observe” for changes. Therefore using useEffect will work as componentDidMount and componentDidUpdate
And how we clear those effects? well, the function that runs in the effect should return another function that is gonna we ran when the component is unmounted. Magic!!
import React, { useState, useEffect } from 'react'
export default function FunctionalTimer (props) {
const [count, setCount] = useState(props.initialCount || 0)
const [running, setRunning] = useState(false)
const start = () => setRunning(true)
const pause = () => setRunning(false)
const reset = () => setCount(0)
const tick = () => running && setTimeout(() => setCount(count + 1), 1000)
useEffect(() => {
tick()
}, [running, count])
return (
<div className="counter">
<span className="counter\_\_count">{ count }</span>
<button onClick={start}>Start</button>
<button onClick={pause}>Pause</button>
<button onClick={reset}>Clear</button>
</div>
)
}
setTimeout and setInterval are effects outside of the pure nature of the functional component. The same applies to things that are changing other things outside the component, for example, the document.title or adding an event listener to a not synthetic event.
import React, { useState, useEffect } from 'react'
export default function FunctionalTimer (props) {
const [count, setCount] = useState(props.initialCount || 0)
const [running, setRunning] = useState(false)
const start = () => setRunning(true)
const pause = () => setRunning(false)
const reset = () => setCount(0)
const tick = () => running && setTimeout(() => setCount(count + 1), 1000)
useEffect(() => {
tick()
}, [running, count])
useEffect(() => {
document.title = `${count} seconds pass`
}, [count])
useEffect(() => {
const logOnSizeUpdate = () => console.log({ count, running })
window.addEventListener('resize', logOnSizeUpdate)
return () => window.removeEventListener('resize', logOnSizeUpdate)
}, []) // when the array is empty only runs on mount not update
return (
<div className="counter">
<span className="counter\_\_count">{ count }</span>
<button onClick={start}>Start</button>
<button onClick={pause}>Pause</button>
<button onClick={reset}>Clear</button>
</div>
)
}
One of the awesome abilities of Hooks is the ability to create Custom Hooks, and custom hooks are just simple functions!
Let's rewrite that timer code and move all the state logic to custom hooks called useTimer and useSetTitle
import React, { useState, useEffect } from 'react'
function useTimer (initialCount = 0, autoStart = false) {
const [count, setCount] = useState(initialCount)
const [running, setRunning] = useState(autoStart)
const start = () => setRunning(true)
const pause = () => setRunning(false)
const reset = () => setCount(0)
const tick = () => running && setTimeout(() => setCount(count + 1), 1000)
useEffect(() => {
tick()
}, [running, count])
return {
count,
running,
start,
pause,
reset
}
}
function useSetTitle (count) {
useEffect(() => {
document.title = `${count} seconds pass`
})
}
export default function FunctionalTimer (props) {
const timer = useTimer(props.initialCount, props.autoStart)
useSetTitle(timer.count)
return (
<div className="counter">
<span className="counter\_\_count">{ timer.count }</span>
<button onClick={timer.start}>Start</button>
<button onClick={timer.pause}>Pause</button>
<button onClick={timer.reset}>Clear</button>
</div>
)
}
This allows us to build a bunch of stateful logic that live in Hooks and can be used in any functional component, enabling us to reuse and compose in a better way our code.
useRef
This hook allows us to have a persistent value between components render. And why we could need that, if you notice in the previous example the way the time is working is putting on setTimeout the next counter change. The problem with that is if we clear the timer, the next time the function that will be run, will have the value of count before the clear, and because you need to pause, clear and start again. Functions references in the effect will have the value of the variables at the moment they were set. That way we will need useRef to storage a callback every time that the component renders with the current count value that maintains the stability of the timer.
import React, { useState, useEffect, useRef } from 'react'
function useTimerInterval (initialCount = 0, autoStart = false) {
const [count, setCount] = useState(initialCount)
const [running, setRunning] = useState(autoStart)
const cb = useRef()
const id = useRef()
const start = () => setRunning(true)
const pause = () => setRunning(false)
const reset = () => setCount(0)
function callback () {
setCount(count + 1)
}
// Save the current callback to add right number to the count, every render
useEffect(() => {
cb.current = callback
})
useEffect(() => {
// This function will call the cb.current, that was load in the effect before. and will always refer to the correct callback function with the current count value.
function tick() {
cb.current()
}
if (running && !id.current) {
id.current = setInterval(tick, 1000)
}
if (!running && id.current) {
clearInterval(id.current)
id.current = null
}
return () => id.current && clearInterval(id.current)
}, [running])
return {
count,
start,
pause,
reset
}
}
function useSetTitle (count) {
useEffect(() => {
document.title = `${count} seconds pass`
})
}
export default function FunctionalTimer (props) {
const timer = useTimerInterval(props.initialCount, props.autoStart)
useSetTitle(timer.count)
return (
<div className="counter">
<span className="counter\_\_count">{ timer.count }</span>
<button onClick={timer.start}>Start</button>
<button onClick={timer.pause}>Pause</button>
<button onClick={timer.reset}>Clear</button>
</div>
)
}
Here is a live example of the code in this post:
And maybe you want to read a little more about why the timer needs the persistent reference. I that case this article deeps into it https://overreacted.io/making-setinterval-declarative-with-react-hooks/
This is a basic example of what we can do know with React Hooks and I recommend to see this video from the React Conf about Hooks.
I will continue this topic about React Hooks with useContext and useReducer and how we can build something like Redux with those two Hooks.
Until the next post, May the Force be with you
Top comments (0)