Using useEffect effectively
David Khourshid · @davidkpiano
stately.ai
React Advanced 2022
[]
old class component example
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
componentDidMount() {
fetchData('/some/data').then((res) => {
this.setState({ data: res });
});
}
}
const [data, setData] = useState(null);
useEffect(async () => {
const res = await fetchData('/some/data');
setData(res);
});
const [data, setData] = useState(null);
useEffect(() => {
fetchData('/some/data')
.then(res => {
setData(res)
});
});
const [data, setData] = useState(null);
useEffect(() => {
fetchData('/some/data')
.then(res => {
setData(res)
});
}, []);
const [data, setData] = useState(null);
useEffect(() => {
fetchData('/some/data/' + id)
.then(res => {
setData(res)
});
}, [id]);
id: 1
id: 2 (cached)
const [data, setData] = useState(null);
useEffect(() => {
let isCanceled = false;
fetchData('/some/data/' + id)
.then(res => {
if (!isCanceled) { return; }
setData(res)
});
return () => { isCanceled = true; }
}, [id]);
Effects execute twice on mount (in strict mode)!
cleanup
┬─┬ノ( º _ ºノ)
Unmount (simulated)
effect
(╯°□°)╯︵ ┻━┻
Remount
effect
(╯°□°)╯︵ ┻━┻
Mount
useEffect()
useDefect()
useFoot(() => { setGun(true); 🦶🔫 });
How does useEffect work?
It doesn't.
.. for many effects.
-
React 18 double exec 🚶♂️👈 🚪
-
[long, dependency, arrays]
-
if (!simple && conditionals)
-
Missing () => { cleanup functions }
-
Ad-hoc setState('calls')
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.
"Effects"
"events"
Activity effects
Action effects
Synchronization with
activity effects.
What is useEffect() for?
useEffect(() => {
const handler = (event) => {
// do something
}
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
}
}, []);
Where do action
effects go?
function Component(props) {
useEffect(() => {
// ...
return () => {/* ... */}
}, [/* ... */]);
// ...
return (
<div>{/* ... */}</div>
);
}
???
No side-effects in render
useEffect (awkward)
Outside the component?
Event handlers.
Sorta.
<form onSubmit={event => {
// 💥 side-effect!
submitData(event);
}}>
{/* ... */}
</form>
Where do action
effects go?
As close as possible
to the event
eventHandler()
someEffect
someEffect
someEffect
someEffect
Action effects happen
outside of rendering.
const FancyInput = ({ isFocused }) => {
const ref = useRef();
useEffect(() => {
if (isFocused) {
ref.current?.focus();
}
}, [isFocused]);
return (
<div>
{/* If you blur, isFocused={true} is a lie */}
<input ref={ref} />
</div>
);
};
const App = () => {
// Focus state is duplicated (and possibly wrong)
const [inputFocused, setInputFocused] = useState(false);
return (
<main>
<FancyInput isFocused={inputFocused} />
{/* What if there was another focused input? */}
{/* Impossible state: multiple focused elements? */}
{/* <FancyInput isFocused={true} /> */}
<button
onClick={() => {
setInputFocused(true);
}}
>
What the focus
</button>
</main>
);
}
const FancyInput = forwardRef(function FancyInput(props, ref) {
return (
<div>
<input ref={ref} />
</div>
);
});
function App() {
const inputRef = useRef();
// Focusing is a fire-and-forget effect that does not need useEffect
function handleButtonClick() {
inputRef.current?.focus();
}
return (
<main>
<FancyInput ref={inputRef} />
<button onClick={handleButtonClick}>Hocus focus</button>
</main>
);
}
"Declarative"
"Imperative"
- When something happens,
- execute this effect.
- When something happens,
- it will cause the state to change
- and depending on which parts of the state changed,
- this effect should be executed,
- but only if some condition is true.
- And React may execute it again
- for some future reason.
- But only in Strict mode!
- Which you shouldn't disable
- for some future reason.
Dependency array
beta.reactjs.org
You don't need useEffect for
transforming data.
useEffect() ➡️ useMemo()
function Cart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(
items.reduce((currentTotal, item) => {
return currentTotal + item.price;
}, 0)
);
}, [items]);
// ...
}
❌
function Cart() {
const [items, setItems] = useState([]);
const total = useMemo(
() =>
items.reduce((currentTotal, item) => {
return currentTotal + item.price;
}, 0),
[items]
);
// ...
}
function Cart() {
const [items, setItems] = useState([]);
const total = items.reduce((currentTotal, item) => {
return currentTotal + item.price;
}, 0);
// ...
}
import React, { useState, useEffect } from 'react';
function Example() {
const [value, setValue] = useState("");
const [count, setCount] = useState(-1);
useEffect(() => {
setCount(count + 1)
});
const onChange = ({ target }) => setValue(target.value);
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>Number of changes: {count}</div>
</div>
);
}
You don't need useEffect for
communicating with parents.
useEffect() ➡️ eventHandler()
function Product({ onOpen, onClose }) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
onOpen();
} else {
onClose();
}
}, [isOpen]);
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen);
}}
>
Toggle quick view
</button>
</div>
);
}
❌
click
state change
effect
function Product({ onOpen, onClose }) {
const [isOpen, setIsOpen] = useState(false);
function toggleView() {
const nextIsOpen = !isOpen;
setIsOpen(!isOpen);
if (nextIsOpen) {
onOpen();
} else {
onClose();
}
}
return (
<div>
<button onClick={toggleView}>Toggle quick view</button>
</div>
);
}
click
effect
function useToggle({ onOpen, onClose }) {
const [isOpen, setIsOpen] = useState(false);
function toggler() {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen);
if (nextIsOpen) {
onOpen();
} else {
onClose();
}
}
return [isOpen, toggler];
}
function Product({ onOpen, onClose }) {
const [isOpen, toggler] = useToggle({ onOpen, onClose });
return (
<div>
<button onClick={toggler}>Toggle quick view</button>
</div>
);
}
You don't need useEffect for
subscribing to external stores.
useEffect() ➡️ useSyncExternalStore()
function Store() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected');
});
return () => {
sub.unsubscribe();
};
}, []);
// ...
}
❌
function Product({ id }) {
const isConnected = useSyncExternalStore(
// subscribe
storeApi.subscribe,
// get snapshot
() => storeApi.getStatus() === 'connected',
// get server snapshot
true
);
// ...
}
You don't need useEffect for
fetching data.
useEffect() ➡️ renderAsYouFetch()
import { getItems } from '../storeApi';
function Store() {
const [items, setItems] = useState([]);
useEffect(() => {
let isCanceled = false;
getItems().then((data) => {
if (isCanceled) return;
setItems(data);
});
return () => {
isCanceled = true;
};
});
// ...
}
❌
import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";
import { getItems } from '../storeApi';
export const loader = async () => {
const items = await getItems();
return json(items);
}
export default function Store() {
const items = useLoaderData();
// ...
}
import { getItems } from '../storeApi';
function Store({ items }) {
// ...
}
export async function getServerSideProps() {
const items = await getItems();
return { props: { items } }
}
export default Store;
import { getItems } from '../storeApi';
import { useQuery, useQueryClient } from 'react-query';
function Store() {
const queryClient = useQueryClient()
// ...
return (
<button onClick={() => {
queryClient.prefetchQuery('items', getItems);
}}>
See items
</button>
);
}
function Items() {
const { data } = useQuery('items', getItems);
// ...
}
useEffect()
useQuery()
useSWR()
use()
⁉️
This enables React developers to access arbitrary asynchronous data sources with Suspense via a stable API.
const fetchPost = cache(async (id) => {
// ...
})
function Post({ id }) {
const post = use(fetchPost(id))
return (
<article>
<h1>{post.title}</h1>
<PostContent post={post} />
</article>
);
}
🏃♀️ Race conditions
⏪ No instant back button
🔍 No initial HTML content
🌊 Chasing waterfalls
Fetching in useEffect problems
You don't need useEffect for
initializing global singletons.
useEffect() ➡️ justCallIt()
function Store() {
useEffect(() => {
storeApi.authenticate();
}, []);
// ...
}
☝️ This will run twice!
❌
function Store() {
const didAuthenticateRef = useRef();
useEffect(() => {
if (didAuthenticateRef.current) {
return;
}
storeApi.authenticate();
didAuthenticateRef.current = true;
}, []);
}
🚩
Ref flag
let didAuthenticate = false;
function Store() {
useEffect(() => {
if (didAuthenticate) {
return;
}
storeApi.authenticate();
didAuthenticate = true;
}, []);
}
Potential wasted
render
storeApi.authenticate();
function Store() {
// ...
}
if (typeof window !== 'undefined') {
storeApi.authenticate();
}
function Store() {
// ...
}
function renderApp() {
if (typeof window !== 'undefined') {
storeApi.authenticate();
}
appRoot.render(<Store />);
}
renderApp();
You don't need useEffect for
handling user events.
useEffect() ➡️ eventHandler()
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (!isLoading || !formData) { return; }
let isCanceled = false;
submitData(event)
.then(() => {
if (isCanceled) { return; }
setIsLoading(false);
})
.catch(err => {
setIsLoading(false);
setError(err);
});
return () => {
isCanceled = true;
}
}, [isLoading, formData]);
<form onSubmit={event => {
setIsLoading(true);
setFormData(event);
}}>
{/* ... */}
</form>
❌
<form onSubmit={event => {
// 💥 side-effect!
submitData(event);
}}>
{/* ... */}
</form>
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
<form onSubmit={event => {
if (isLoading) { return; }
// 💥 side-effect!
submitData(event)
.then(() => { setIsLoading(false) })
.catch(err => {
setIsLoading(false);
setError(err);
});
setIsLoading(true);
}}>
{/* ... */}
</form>
const [state, send] = useCheckoutForm();
<form onSubmit={event => {
send({ type: 'submit', data: event });
}}>
{/* ... */}
</form>
const videoRef = useRef();
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
if (isPlaying) {
videoRef.current?.play();
} else {
videoRef.current?.pause();
}
}, [isPlaying]);
useEffect(() => {
if (isPlaying) {
const handler = () => {
setIsPlaying(false);
};
videoRef.current.addEventListener("ended", handler);
return () => {
videoRef.current?.removeEventListener("ended", handler);
};
}
}, [isPlaying]);
useEffect(() => {
if (isPlaying) {
const handler = (event) => {
if (event.key === "Escape") {
setIsPlaying(false);
}
};
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}
}, [isPlaying]);
mini
full
toggle / playVideo
toggle / pauseVideo
switch (state) {
case 'mini':
if (event.type === 'toggle') {
playVideo();
return 'full';
}
break;
case 'full':
if (event.type === 'toggle') {
pauseVideo();
return 'mini';
}
break;
default:
break;
}
Where do action effects go?
Event handlers.
In state transitions.
...which happen to be executed at the same time as event handlers.
useEffect is for synchronization
useEffect is for synchronization
Activity effects go in useEffect
useEffect is for synchronization
Activity effects go in useEffect
Action effects go in event handlers
useEffect is for synchronization
Activity effects go in useEffect
Action effects go in event handlers
Render-as-you-fetch
useEffect is for synchronization
Activity effects go in useEffect
Action effects go in event handlers
Render-as-you-fetch
State transitions trigger effects
useEffect(() => { // my talk return () => {
} }, [])
Using useEffect effectively
By David Khourshid
Using useEffect effectively
- 1,242