Goodbye, useEffect
David Khourshid · @davidkpiano
stately.ai
👋
React Brussels 2022
:-)
)-':
:-/
[]);
useEffect(() => {
// DANGER ZONE
}, []);
function Component(props) {
useEffect(() => {
// Do something
return () => {/* Cleanup */}
}, [/* Dependencies */]);
// ...
return (
<div>{/* ... */}</div>
);
}
×2
React 18 runs effects
twice on mount
(in strict mode)
is not for all effects.
useEffect()
🤔
componentDidMount
componentDidUpdate
componentWillUnmount
useEffect(() => {
// componentDidMount?
}, []);
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing]);
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, []);
useEffect is not a lifecycle hook.
Dependency array
useEffect(() => {
doSomething();
}, [whenever, these, things, change])
Effect
"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
useEffect(() => {
doSomething();
return () => cleanup();
}, [whenThisChanges]);
Ideal
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething();
} else {
doSomethingElse();
}
// oops, forgot the cleanup
}, [foo, bar, baz, quo]);
Ideal Reality
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false);
}
setRemovingValue(false);
}, [removingValue, hasValue, cssDisplayFlex]);
const isVisible = useOnScreen(ref)
const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite(
(...args) => getKey(...args, repo, PAGE_SIZE),
fetcher
)
const issues = data ? [].concat(...data) : []
const isLoadingInitialData = !data && !error
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined')
const isEmpty = data?.[0]?.length === 0
const isReachingEnd = size === PAGE_SIZE
const isRefreshing = isValidating && data && data.length === size
useEffect(() => {
if (isVisible && !isReachingEnd && !isRefreshing) {
setSize(size + 1)
}
}, [isVisible, isRefreshing])
Dependences are the wrong
mental model for effects.
function Component(props) {
useEffect(() => {
// Do something
return () => {/* Cleanup */}
}, [/* Dependencies */]);
// ...
return (
<div>{/* ... */}</div>
);
}
React 18 runs effects
twice on mount
(in strict mode)
How to fix this?
×2
cleanup
┬─┬ノ( º _ ºノ)
Unmount (simulated)
effect
(╯°□°)╯︵ ┻━┻
Remount
effect
(╯°□°)╯︵ ┻━┻
Mount
useEffect()
useDefect()
useFoot(() => { setGun(true); 🦶🔫 });
What is useEffect() for?
Synchronization.
useEffect(() => {
const sub = createThing(input).subscribe(value => {
// do something with value
});
return sub.unsubscribe;
}, [input]);
What is useEffect() for?
Synchronization.
const [itemData, setItemData] = useState(null);
useEffect(() => {
// Synchronize with external system
const sub = storeApi.subscribeToItem(itemId, setItemData);
// Subscription disposal
return sub.unsubscribe;
}, [itemId]); // Subscription dependency
Subscriptions can resubscribe
multiple times!
Action effects
Activity effects
"Fire-and-forget"
Synchronized
External
system
Activity effects
Synchronized
Unmount
Remount
Where do action
effects go?
function Component(props) {
useEffect(() => {
// ...
return () => {/* ... */}
}, [/* ... */]);
// ...
return (
<div>{/* ... */}</div>
);
}
???
No side-effects in render
useEffect (awkward)
Outside the component?
eventHandler()
someEffect
someEffect
someEffect
someEffect
Action effects happen
outside of rendering.
Where do action effects go?
Event handlers.
Sorta.
<form onSubmit={event => {
// 💥 side-effect!
submitData(event);
}}>
{/* ... */}
</form>
Event happens
Effect
Event happens
State changes
Effect
State changes
State changes
Effect
Event handler
useEffect
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 = items.reduce((currentTotal, item) => {
return currentTotal + item.price;
}, 0);
// ...
}
function Cart() {
const [items, setItems] = useState([]);
const total = useMemo(
() =>
items.reduce((currentTotal, item) => {
return currentTotal + item.price;
}, 0),
[items]
);
// ...
}
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>
);
}
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>
);
}
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;
}, []);
}
let didAuthenticate = false;
function Store() {
useEffect(() => {
if (didAuthenticate) {
return;
}
storeApi.authenticate();
didAuthenticate = true;
}, []);
}
storeApi.authenticate();
function Store() {
// ...
}
if (typeof window !== 'undefined') {
storeApi.authenticate();
}
function Store() {
// ...
}
function renderApp() {
if (typeof window !== 'undefined') {
storeApi.authenticate();
}
appRoot.render(<Store />);
}
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);
<form onSubmit={event => {
if (isLoading) { return; }
// 💥 side-effect!
submitData(event);
setIsLoading(true);
}}>
{/* ... */}
</form>
const [isLoading, setIsLoading] = useState(false);
<form onSubmit={event => {
if (isLoading) { return; }
// 💥 side-effect!
submitData(event)
.then(() => { setIsLoading(false) })
setIsLoading(true);
}}>
{/* ... */}
</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>
Demo time
stately.ai/studio
When do effects happen?
State transitions.
Always.
Middleware, callbacks, sagas, reactions, sinks, monads (?), whenever
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
State transitions trigger effects
useEffect is for synchronization
State transitions trigger effects
Action effects go in event handlers
useEffect is for synchronization
State transitions trigger effects
Action effects go in event handlers
Render-as-you-fetch
useEffect is for synchronization
State transitions trigger effects
Action effects go in event handlers
Render-as-you-fetch
Model effects with state machines
Thank you React Brussels!
Goodbye, useEffect
By David Khourshid
Goodbye, useEffect
- 1,151