28
loading...
This website collects cookies to deliver better user experience
(caption: Mrs. Tedesco writing a Todo Application in React with hooks)
useEffect
hooks. We intend to just run some code when X changes, but then ESLint tells us to add Y and Z to the dependency list.useEffect(() => {
setCount(count + 1)
// eslint-disable-next-line
}, [])
count
gets a value from the past). But most importantly, it hides bad design in other parts of the component.useEffect
can be made compliant with the ESLint rule while still mantaining the desired behavior. The solutions might not be straightforward, but it is always better to change other parts of the code than to add the rule. The overal benefit will be more consistent code.useEffect
is mostly about updating derived state.function Page({ id, mode }: { id: number; mode: 'read' | 'edit' }) {
const [formData, setFormData] = useState<null|FormData>(null)
const handleError = useErrorHandler()
useEffect(() => {
loadFormContents(id, mode)
.then(setFormData)
.catch(handleError)
}, [id, mode])
if (!formData) return null;
return <TheForm formData={formData} />
}
(*) when I use the "derived state" idiom here, I am mentioning 2 variables which have some dependency between them. This is a general UI term, forget any old React methods that used this name.
id
. What if the page route changes, bringing in a new id
? We need to handle the prop change.useEffect
can also happen with an empty dependency array, which showcases that it is also needed for async behavior, even when there's no derived state.setState
from useState()
and dispatch
from Redux. Dispatchers from other React libs are usually expected to be stable.useCallback
and useMemo
. Forgetting to use useCallback
on a function and then feeding it to useEffect
can lead to a disaster.setState
.const [state, setState] = useState({ id: 2, label: 'Jessica' })
// good
useEffect(() => {
setState(previous => ({ ...previous, name: 'Jenn' }))
}, [])
// bad
useEffect(() => {
setState({ ...state, name: 'Jenn' })
}, [state])
state
variable from the array (setState
is already recognized as stable by the plugin).useEffect
is made to handle derived state.A
and B
based on 1
and 2
.1, 2 <-- A, B
A
depends on 1
but not on 2
? In this case, we can split a big useEffect
into smaller ones.1 <-- A
2 <-- B
function Component({ userId, event }: { userId: number, event: Event }) {
const [subscriptionIsExpired, setSubscriptionExpired] = useState(false)
useEffect(() => {
const userSettings: { validUntil: string } = await getUserSettings(userId)
const isExpired = event.startDate > userSettings.validUntil
setSubscriptionExpired(isExpired)
}, [userId, event])
return (...)
}
getUserSettings()
request will be called when event
changes. But it actually has nothing to do with the event
. We may refactor that to:function Component({ userId, event }: { userId: number, event: Event }) {
const [userSettings, setUserSettings] = useState<null|UserSettings>(null)
const [subscriptionIsExpired, setSubscriptionExpired] = useState<null|boolean>(null)
useEffect(() => {
const userSettings: { validUntil: string } = await getUserSettings(userId)
setUserSettings(userSettings)
}, [userId])
useEffect(() => {
if (!userSettings) {
return
}
const isExpired = event.startDate > userSettings.validUntil
setSubscriptionExpired(isExpired)
}, [userSettings, event])
return (...)
}
userId
. The second effect continues to depend on both userId
(through userSettings
) and event
.from:
userId, event <-async-- isExpired
to:
userId <-async- userSettings
event, userSettings <-- isExpired
eslint-disable
by copying the dependency to a state or to a ref.function Component({ id }) {
// gets the value from the first render
const [initialId] = useState(id) // or useState(() => id)
useEffect(() => {
// ...
}, [initialId])
return (...)
}
A state is tied to a render through lexical scope. Each render can reference a different state object from a different slice of time; This may have impact on future concurrent render modes?
A ref is just a property tied to the component. ref.current
will always point to the same thing and will always be current, regardless of where you call it;
useEffect
, so you could get rid of a dependency by turning it into a ref. I'd pin out the following properties of something that can likely be turned into a ref:const [trigger, forceEffect] = useState({})
useEffect(() => {
// some code here
}, [trigger])
return <button onClick={() => forceEffect({})}>
Force effect
</button>