David Khourshid
@davidkpiano · stately.ai
// DANGER ZONE
useImperativeHandle()
useEffect()
useEff this
useEffect()
🤔
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"
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.
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); });
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
<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>
(state, event) => nextState
effects...?
(state) => UI
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
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
...which happen to be executed at the same time as event handlers.
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
const executedRef = useRef(false);
useEffect(() => {
if (executedRef.current) { return; }
doSomething();
executedRef.current = true;
}, [/* ... */]);
false
true
🚩
(sorry, useSWR & useQuery)
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 // ...
}
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>
);
}
(state, event) => (nextState, )
effects