:-)
)-':
:-/
[]);
useEffect(() => {
// ZONA PELIGROSA
}, []);
useImperativeHandle()
useEffect()
useEff this
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.
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>
);
}
useEffect is not
a state setter.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
No dependency array!
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.
useEffect(() => {
const sub = createThing(input).subscribe(value => {
// do something with value
});
return sub.unsubscribe;
}, [input]);
const [itemData, setItemData] = useState(null);
useEffect(() => {
// Synchronize with external system
const sub = storeApi.subscribeToItem(itemId, setItemData);
// Subscription disposal
return sub.unsubscribe;
}, [itemId]); // Subscription dependency
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
UI is a function of state
(state, event) => nextState
effects...?
(state) => UI
(state, event) => (nextState, )
effects
(state, event) => nextState
🍵
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.
beta.reactjs.org
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>
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]
);
// ...
}
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(
storeApi.subscribe,
() => storeApi.getStatus() === 'connected',
true
);
// ...
}
import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';
// ..
const machine = createMachine({
// ...
});
const CheckoutForm = () => {
const [state, send] = useMachine(machine);
return (
<form onSubmit={() => send('SUBMIT')}>
{state.matches('shipping') && <Shipping />}
{state.matches('billing') && <Billing />}
{state.matches('review') && <Review />}
{state.matches('receipt') && <Receipt />}
</form>
);
}
⬅ useSyncExternalStore()
npm i @xstate/react
import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';
const machine = createMachine({
initial: 'first',
states: {
first: {
entry: doSomething,
// ...
},
// ...
submitting: {
invoke: {
src: (context) => submitForm(context.data),
onDone: {
actions: logAnalytics,
target: 'submitted'
}
}
},
submitted: {
// ...
}
}
});
Entry + exit actions
Invocations (activities)
Transition actions
npm i xstate
Demo time
stately.ai/editor
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);
// ...
}
🏃♀️ 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 />);
}
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
¡Gracias, React Alicante!
Adios, useEffect
By David Khourshid
Adios, useEffect
- 1,136