Goodbye, useEffect
David Khourshid
@davidkpiano · stately.ai
:-)
)-':
:-/
[]);
useEffect(() => {
// DANGER ZONE
}, []);
useImperativeHandle()
useEffect()
useEff this
is not for effects*
useEffect()
🤔
componentDidMount
componentDidUpdate
componentWillUnmount
useEffect(() => {
// componentDidMount?
}, []);
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing]);
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, []);
useEffect is not a lifecycle hook.
https://twitter.com/tlakomy/status/1501574622839463936
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(() => {
/* do this effect */
}, [/* whenever something here changes */])
"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
no reasonconcurrent rendering.
Dependency array
useEffect(() => {
doSomething();
return () => cleanup();
}, [whenThisChanges]);
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething();
} else {
doSomethingElse();
}
// oops, forgot the cleanup
}, [foo, bar, baz, quo]);
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.
Where do
effects go?
function Component(props) {
useEffect(() => {
// ...
return () => {/* ... */}
}, [/* ... */]);
// ...
return (
<div>{/* ... */}</div>
);
}
???
No side-effects in render
useEffect (awkward)
Outside the component?
function Component(props) {
useEffect(() => {
// ...
return () => {/* ... */}
}, [/* ... */]);
// ...
return (
<div>{/* ... */}</div>
);
}
×2
React 18 runs effects
twice on mount
(in strict mode)
effect
(╯°□°)╯︵ ┻━┻
Mount
cleanup
┬─┬ノ( º _ ºノ)
Unmount (simulated)
effect
(╯°□°)╯︵ ┻━┻
Remount
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]);
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
};
elRef.current.addEventListener('pointermove', handler);
return () => {
elRef.current.removeEventListener('pointermove', handler);
}
}, []);
Action effects
Activity effects
"Fire-and-forget"
Synchronized
Activity effects
Synchronized
Unmount
Remount
Where do action effects go?
Event handlers.
Sorta.
<form onSubmit={event => {
// 💥 side-effect!
submitData(event);
}}>
{/* ... */}
</form>
eventHandler()
someEffect
someEffect
someEffect
someEffect
Effects happen outside
of rendering.
<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, dispatch] = useFormReducer();
<form onSubmit={event => {
dispatch(event);
}}>
{/* ... */}
</form>
UI is a function of state
(state, event) => nextState
effects...?
(state) => UI
When do effects happen?
State transitions.
Always.
Middleware, callbacks, sagas, reactions, sinks, monads (?), whenever
(state, event) => (nextState, )
effects
(state, event) => nextState
🍵
idle
loading
LOAD / fetchData
(idle, LOAD) => (loading, [ ])
fetchData
idle
loading
LOAD / fetchData
success
RESOLVE / assign
RELOAD / fetchData
idle
loading
LOAD
success
RELOAD
entry / fetchData
exit / logTelemetry
RESOLVE / assign
- ➡️ Transition actions
- 📥 Entry actions
- 📤 Exit actions
import { useState, useCallback } from 'react';
function useSpicyReducer(reducer, initialState, executeEffect) {
const [state, setState] = useState(initialState);
const spicyDispatch = useCallback(
(event) => {
// Calculate next state
const nextState = reducer(state, event);
// Execute effect based on transition
executeEffect(state, event, nextState);
// Commit next state
setState(nextState);
},
[reducer, state, executeEffect]
);
return [state, spicyDispatch];
}
⬅ Exercise left to reader
Where do action effects go?
Event handlers.
In state transitions.
...which happen to be executed at the same time as event handlers.
Effects with external stores
useSyncExternalStore()
store
store.whatever(...)
sync
interact
Component
import { useSyncExternalStore } from 'react';
import { someStore } from './somewhere';
function Component() {
const state = useSyncExternalStore(
someStore.subscribe,
someStore.getSnapshot);
return (
<button onClick={() => {
someStore.dispatch({ type: 'someEvent' })
}>
Click me
</button>
);
}
import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';
const machine = createMachine({
// ...
});
const Wizard = () => {
const [state, send] = useMachine(machine);
return (
<form onSubmit={() => send('SUBMIT')}>
{state.matches('first') && <FirstStep />}
{state.matches('second') && <SecondStep />}
{state.matches('review') && <ReviewStep />}
{state.matches('submitting') && <Submitting />}
</form>
);
}
⬅ useSyncExternalStore()
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
State management
State orchestration
- State store
- State updates
- Subscriptions
- Event dispatching
- Multiple stores
- Communication
- Effect management
- Finite states + transitions
Execute on mount, only once
const executedRef = useRef(false);
useEffect(() => {
if (executedRef.current) { return; }
doSomething();
executedRef.current = true;
}, [/* ... */]);
false
true
🚩
Fetch-on-render
Fetching data
Render-as-you-fetch with Suspense.
(sorry, useSWR & useQuery)
is a lie.
Fetching data with Suspense
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
let canceled = false;
fetch('some/resource').then(data => {
if (canceled) return;
setData(data);
});
return () => { canceled = true }
}, []);
return // ...
}
function Component() {
const data = someResource.read(); // might suspend
return // ...
}
Fetching data
function Component() {
const [isSubmitting, setIsSubmitting]
= useState(false);
const [error, setError] = useState(null);
return (
<form onSubmit={event => {
if (isSubmitting) { return; }
// 💥 side-effect!
submitData(event)
.then(() => { setIsLoading(false) })
.catch(err => {
setIsSubmitting(false);
setError(err);
});
setIsSubmitting(true);
}}>
{/* ... */}
</form>
);
}
function Component() {
const [isSubmitting, setIsSubmitting]
= useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (isSubmitting) { return; }
// 💥 side-effect!
submitData(event)
.then(() => { setIsLoading(false) })
.catch(err => {
setIsSubmitting(false);
setError(err);
});
setIsSubmitting(true);
}, [isSubmitting]);
return (
<form onSubmit={event => {
setIsSubmitting(true);
}}>
{/* ... */}
</form>
);
}
function Component() {
const [isSubmitting, setIsSubmitting]
= useState(false);
const [error, setError] = useState(null);
return (
<form onSubmit={event => {
if (isSubmitting) { return; }
// 💥 side-effect!
submitData(event)
.then(() => { setIsLoading(false) })
.catch(err => {
setIsSubmitting(false);
setError(err);
});
setIsSubmitting(true);
}}>
{/* ... */}
</form>
);
}
import { useFormStore } from './somewhere';
function Component() {
const [state, send] = useFormStore();
return (
<form onSubmit={event => {
send(submitEvent(event));
}}>
{/* ... */}
</form>
);
}
Effects are state management.
Don't put side-effects aside.
(state, event) => (nextState, )
effects
useEffect is for synchronization
useEffect is for synchronization
State transitions trigger effects
useEffect is for synchronization
State transitions trigger effects
Effects go in event handlers
useEffect is for synchronization
State transitions trigger effects
Effects go in event handlers
Render-as-you-fetch (suspense)
useEffect is for synchronization
State transitions trigger effects
Effects go in event handlers
Render-as-you-fetch (suspense)
Model effects with state machines
Thank you, Reactathon!
Goodbye, useEffect
By David Khourshid
Goodbye, useEffect
- 3,853