| auto-scaling | class | marp | size | theme |
|---|---|---|---|---|
true |
lead |
true |
58140 |
default |
useEffect: let's adjust the shot! ๐ฏ
17/12/2019
useEffecttakes effects as input.- Effects are functions.
- Effects are an escape hatch from Reactโs purely functional world.
By default, effects run after every completed render.
useEffect(() => {
console.log('Effect!')
})We can choose to fire effects only when certain values have changed.
useEffect(() => {
console.log('Effect!')
}, [])Golden (but hidden) rule about useEffect
We shouldn't translate components lifecycles into useEffect hooks.
- The entire web speaks about how to perform this translation.
- The React doc shows plenty of comparisons between them.
- It's true that
useEffecthooks might run during one or more lifecycles - But... this does not mean they should be treated like the same thing! Proof: in SSR
componentDidMountdoes not run.useEffectdoes.
class Lifecycles extends Component {
state = {
transformedData: null,
}
componentWillReceiveProps = ({ data: newData }: { data: string[] }) => {
const { data } = this.props
if (data !== newData) {
const transformedData = doSomethingRealHeavyWithData(newData)
this.setState({ transformedData })
}
}
render = () => (
// some JSX here
)
}const Hooks = ({ data }: { data: string[] }) => {
const [transformedData, setTransformedData] = useState(null)
useEffect(() => {
const newData = doSomethingRealHeavyWithData(data)
setTransformedData(newData)
}, [data])
return (
// some JSX here
)
}const BetterHooks = ({ data }: {ย data: string[]ย }) => {
const transformedData = useMemo(() => (
doSomethingRealHeavyWithData(data)
), [data])
return (
// some JSX here
)
}- If you specify a list of dependencies as the last argument to
useEffect,useMemo,useCallback, it must include all values used inside that participate in the React data flow. - React doc. - That includes props, state, and anything derived from them. - React doc.
- If there is an eslint rule about it, there must be a reason. - lucarge.
class Lifecycles extends Component {
state = {
data: null
}
componentDidMount = async ({ input }: { input: string }) => {
const data = await fetchData(input)
this.setState({ data })
}
render = () => (
// some JSX here
)
}const Hooks = ({ input }: { input: string }) => {
const [data, setData] = useState(null)
useEffect(() => {
const run = async () => {
if (data) {
return
}
const freshData = fetch(input)
setData(freshData)
}
run().catch(() => console.log('Ops!'))
}, [])
return (
// use `data` for rendering something
)
}const Hooks = ({ input }: { input: string }) => {
const [data, setData] = useState(null)
useEffect(() => {
const run = async () => {
if (data) {
return
}
const freshData = fetch(input)
setData(freshData)
}
run().catch(() => console.log('Ops!'))
}, [data, input])
return (
// use `data` for rendering something
)
}- Hooks shouldn't be bound to assumptions related to the status quo of the architecture. ๐๐ป
- We shouldn't assume that
inputnever changes, just because at the moment it does not happen. ๐๐ป - This component could be moved somewhere else or its parent could change its behavior or
inputcould change in the global state or something else in the architecture could makeinputchange.
const BetterHooks = ({ input }: { input: string }) => {
const [data, setData] = useState(null)
const previousInput = usePrevious(input)
useEffect(() => {
const run = async () => {
if (!!data && input === previousInput) {
return
}
const freshData = fetch(input)
setData(freshData)
}
run().catch(() => console.log('Ops!'))
}, [data, input, previousInput])
return (
// some JSX here
)
}If we all agree on always specifying all the hooks dependencies,
I'd drop useAsyncEffect since it shadows the exhaustive-deps eslint rule.
Doing without it is not that bad:
useEffect(() => {
const run = async () => true
run().then(console.log).catch(console.log)
})Thanks! ๐๐ป
Questions? ๐ค