What did I learn from the workshop Basic React Hooks by Kent C. Dodds?
Previously, I wrote down my learnings from the React Fundamentals workshop.
The second workshop introduces React hooks.
Here are some insights that I found useful:
Set Initial State Via Props
Set an initial value by adding props to a component: <Greeting initialName="George">
Take that as an argument to the function and pass it down to useState
:
function Greeting({ initialName = '' }) {
cont[(name, setName)] = React.useState(initialValue)
}
// more code
Make then onChange
handler a controlled input: <input onChange={handleChange} id="name" value={name} />
You can also use lazy initialization with a function. If you pass a function as the initial value to the useState
hook, React will call it during first render. Thus you can avoid re-creating the initial state.
(In fact, that’s how the ReasonReact useState
hook works, see here).
The React useEffect
Dependency Array Does Shallow Comparison
You can watch a lesson on egghead.io on how to use the lodash library and useRef
for deep comparison (paid content)).
Or a blog post on dev.to.
Derived State
During one of the exercises, I realized the benefits of deriving state. I may have used it before, but the tic-tac-toe game in the workshop drove it home.
The exercise has an array of squares
as the game board ([squares, setSquares] = React.useState(Array(9).fill(null)
).
I need to handle other statuses like winner
, nextValue
. My natural inclination was to write more useState
hooks for these states.
Instead, it’s more prudent to create functions that calculate these pieces from the squares
state. Every time squares
changes, the functions will fire off because of React’s re-render. And you will get updated (derived) state.
function Board() {
const [squares, setSquares] = React.useState(Array(9).fill(null))
const nextValue = calculateNextValue(squares)
const winner = calculateWinner(squares)
const status = calculateStatus(winner, squares, nextValue)
function selectSquare(square) {
if (winner || squares[square]) {
return
}
const squaresCopy = [...squares]
squaresCopy[square] = nextValue
setSquares(squaresCopy)
}
function selectTwoSquares(square1, square2) {
if (winner || squares[square1] || squares[square2]) {
return
}
const squaresCopy = [...squares]
squaresCopy[square1] = nextValue
squaresCopy[square2] = nextValue
setSquares(squaresCopy)
}
// return beautiful JSX
}
DOM Interaction with useRef
React consists of two parts: React.createElement
(or its syntactic sugar JSX) and ReactDOM.render
.
The first one (part of the react
package) creates React elements and has nothing to do with the browser’s DOM.
ReactDOM.render
is from the react-dom
package (not the react
package). The react-dom
library “provides DOM-specific methods”.
For accessing DOM nodes directly, there’s a hook called useRef
. A ref
is a reference to a DOM element.
Error Boundaries Can Be Useful
Since React Hooks landed, I have not used class components in new projects.
Error Boundaries are one of the APIs that still work as a class component, and cannot be modeled via Hooks.
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.
Luckily, the react-error-boundary
library exposes a more user-friendly API. You don’t have to write a class-based component (the ugliness….).
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
</div>
)
}
//
function App() {
return (
<div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Greeting />
<Farewell />
</ErrorBoundary>
</div>
)
}
react-error-boundary
also gives you a useErrorHandler
custom hook to handle other errors, which cannot be handled with React’s own Error Boundaries:
- Event handlers (learn more)
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself (rather than its children)
For example, you can do this:
function Greeting() {
const [greeting, setGreeting] = React.useState(null)
const handleError = useErrorHandler()
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
fetchGreeting(name).then(
newGreeting => setGreeting(newGreeting),
handleError,
)
}
return greeting ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit"}>
get a greeting
</button>
</form>
)
}
Further Reading
- Persisting React State in localStorage
- useState lazy initialization and function updates
- Don’t Sync State. Derive It!
- How to update state from props in React
- ReactDOM
- A guide to React refs: useRef and createRef
- Error Boundaries
- Use react-error-boundary to handle errors in React
- Simple error handling in React with react-error-boundary