See Our Public Workshops:
I wanna talk a little bit about useEffect
in React because I absolutely love it and I've noticed a bit of confusion about it in the community.
Side Effects
useEffect
is a new way in React to describe side effects. What's a side effect? Well, if you've got some time this weekend, why don't you give this wikipedia article a read. In short, a side effect is anything that a function does besides returning a value. Here's a function without side effects.
let add = (x, y) => x + y
And it's nice because we can plan on it not doing anything unexpected:
// prettier-ignore[1, 2, 3].map(v => add(v, 1))// [2, 3, 4][1, 2, 3].map(v => add(v, 2))// [3, 4, 5]
Here's one with a side effect:
let add = (x, y) => {Array.prototype.map = () => '🗺'return x + y}
And it's funny cause now:
// prettier-ignore[1, 2, 3].map(v => add(v, 1))// [2, 3, 4][1, 2, 3].map(v => add(v, 2))// "🗺"
Get it? The first map with calls add
which has a "side effect" and changes the semantics of the map
function for the next time it's used. The next map
is now weird and buggy.
So anyway, usually side effects are actually useful. And if you think about it, if a program had nothing but functions without side effects nothing would happen. Eventually you need a side effect for a program to have any utility at all. In the first programs most of us write we console.log()
'd or print
'd.
In React, the primary side effect is rendering to the target platform, like DOM elements.
React Manages Element Side Effects
We don't have to manage the primary side effect in React because that's the whole point of the library. We hand our app off to ReactDOM.createRoot(root).render(<App />)
and it manipulates the DOM for us. The DOM operations like, el.innerHTML = someText
, or el.className = someClassName
are abstracted away. This is why we like React--we get to write the simpler code: inputs and outputs, no side effects.
But eventually an app needs to do more than render elements, and so we actually do need to manage some side-effects like:
- changing the scroll position
- listening to window resize
- managing focus
- saving state to local storage
- synchronizing state to the web audio API
Generally speaking, using an effect in React means to synchronize your app state with anything besides the DOM elements React is managing for you.
Reconciling the Element Tree
Let's back up and think about how React synchronizes your app state to the DOM, and how it reconciles the element tree. Consider this little app: as we edit text, the heading's text changes:
function App() {const [name, setName] = useState('Ryan')return (<div><h1>{name}</h1><inputonChange={(event) => {setName(event.target.value)}}/></div>)}
When we hand this app off to ReactDOM.render
it's going to manage the side effects for us. Everytime we call setName
it will call App()
, compare the old return value (an element tree) to the new one, find the differences, and then call some DOM operations (side effects!) to synchronize your app state to the browser. In this case, it'll probably do something like heading.innerHTML = name
when it needs to.
Let's Get Weird
Alright, how about we take over some of the rendering job ourselves? This is silly to do, but we're just trying to understand useEffect
a bit more.
function App() {let [name, setName] = useState('Ryan')let headingRef = useRef()// we'll update the heading ourselvesuseEffect(() => {let node = headingRef.currentnode.innerHTML = name})return (<div><h1 ref={headingRef} /><inputonChange={(event) => {setName(event.target.value)}}/></div>)}
With useEffect
, we've told React to run a side effect every time App
is rendered. In the end, nothing has changed about the app from the user's perspective, but as the developer we've taken over the DOM manipulation of the heading.
Looking at this code, you can kind of think about ReactDOM.render()
as a useEffect
hook like, I dunno, useReactDOM(<App/>, root)
!
Diffing the "useEffect Tree"
There's one big difference though between these two apps: we manipulate the DOM every render but React only manipulates the DOM when the text is different. (Actually, there's another significant difference, we have a security concern because we used innerHTML
without escaping, whereas React is safe by default).
For example, if we call setName("Brad")
and then setName("Brad")
again, our effect will still perform the DOM operation. Conversely, React would diff the element tree, notice nothing changed, and leave the heading alone on the second call to setName("Brad")
.
That's where the second argument to useEffect
comes into play. It's a way for us to tell React not to perform the side effect unless the values in the array have changed. It's a way for useEffect
to act like the element tree and participate in the "diff" part of React's synchornization of state to the user interface.
useEffect(() => {let node = headingRef.currentnode.innerHTML = name},// React will diff the old `name` and// the new `name` to decide if this// effect should be run again[name])
Now when we setName()
and React calls App()
again, not only will it diff the element tree to decide which of its own side effects need to be run, it will also diff our "effect tree" (the second argument to useEffect
) to decide which of our side effects need to be run.
And it's awesome.
Please note that "effect tree" is not a real React term, its just an analogy in this article.
A Real Use Case
Probably the simplest real use case to understand is updating the document title:
function App() {const [name, setName] = useState('Ryan')useEffect(() => {document.title = name}, [name])// ...}
It's the same ol' React you already know and love, except now with hooks, its smart enough to diff your side effects the same way it diffs your elements--which previously was a lot harder to do on my own.
We're going to be digging into all the hooks in our new workshop this spring, we'd love to see you there :D