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); 🦶🔫 });
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.
useEffect(() => {
const handler = (event) => {
// do something
}
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
}
}, []);
function Component(props) {
useEffect(() => {
// ...
return () => {/* ... */}
}, [/* ... */]);
// ...
return (
<div>{/* ... */}</div>
);
}
???
No side-effects in render
useEffect (awkward)
Outside the component?
<form onSubmit={event => {
// 💥 side-effect!
submitData(event);
}}>
{/* ... */}
</form>
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"
Dependency array
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>
);
}
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>
);
}
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
);
// ...
}
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>
);
}
useEffect() ➡️ justCallIt()
function Store() {
useEffect(() => {
storeApi.authenticate();
}, []);
// ...
}
❌
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();
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;
}
...which happen to be executed at the same time as event handlers.
useEffect(() => { // my talk return () => {
} }, [])