auto-scaling | class | marp | size | theme |
---|---|---|---|---|
true |
lead |
true |
58140 |
default |
useEffect: let's adjust the shot! ๐ฏ
17/12/2019
useEffect
takes 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
useEffect
hooks might run during one or more lifecycles - But... this does not mean they should be treated like the same thing! Proof: in SSR
componentDidMount
does not run.useEffect
does.
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
input
never 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
input
could change in the global state or something else in the architecture could makeinput
change.
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? ๐ค