Is there any potential issues handling componentDidMount and componentDidUpdate with useHooks in this manner?
Two goals here:
Use one useEffect to handle both componentDidMount and componentDidUpdate
No need to pass in the 2nd argument (normally an array with props)
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
}) // <- no need to pass in 2nd argument
There is no issue with it but if any props or state value will change then useEffect will not load on component re-render.
so better to use 2nd argument when this depends on the values change.
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
},[]) // some value as array who will change and this need to be called
Yopu should pass an empty array as an argument
useEffect(() => {
if(once.current === false){
// do things as were in componentDidMount
once.current = true
}
return () => {
// this area will fired when component unmounts
}
}, []) // by providing an empty array this useEffect will act like componentDidMount
If you want to re-render useEffect on a state change you can add the state to the empty array so it will listen changes in the state and will be executed
const [someStateValue, setSomeStateValue] = useState('')
useEffect(() => {
...
}, [someStateValue]) // will be executed if someStateValue changes
If you want to combine componentDidMount and componentDidUpdate, I have a solution in my mind that may work
const [val, setVal] = useState('asd')
const [oldVal, setOldVal] = useState('')
useEffect(() => { // componentDidMount
if(val !== oldVal){ // componentDidUpdate
// pass val to old val
setOldVal(val)
// set new val to new val
setVal("theNewestVal")
}
return () => { // componentWillUnmount
...
}
}, [val]) // will be executed if someStateValue changes
Related
const [panelRight, setPanelRight] = useState(sample(filter(menu, (el) => el !== panelLeft)));
useEffect(() => {
const interval = setInterval(() => {
const menuCategory = filter(menu, (el) => el !== panelLeft && el !== panelRight);
setPanelRight(sample(menuCategory));
}, 10000);
Currently panelRight is being set to the same random menu element every time the the setInterval is called. I think this might be because I need to reference the previous state in the filter method, but how?
Your code lacks details, we dont know what is in your useEffect deps so lets assume and guess. Provide more details next time, please.
Lets assume your useEffect has an empty [] dependencies array and called only once on the first render.
So the issue - setInterval will always hold the value it had initially when the setInterval callback (closure) was defined. Values from the first render.
And here is what you should do:
// Insert that before or after your setStates section
const panelRightRef = React.useRef();
const panelLeftRef = React.useRef();
// That will update ref variables
useEffect(() => {panelRightRef.current = panelRight}, [panelRight]);
useEffect(() => {panelLeftRef.current = panelLeft}, [panelLeft]);
useEffect(() => {
const interval = setInterval(() => {
// if menu is also a State variable and is not immutable - you should add useRef() for it
const menuCategory = filter(menu, (el) => el !== panelLeftRef.current && el !== panelRightRef.current);
setPanelRight(sample(menuCategory));
}, 10000);
// that will be called on depsarray changes or when component is destroyed
return () => clearInterval(interval);
}, []);
I am using the geolocation API inside my functional component, and my success and error callbacks for geolocation.watchPosition(success, error) have conditional code that is dependent on some state value that is going to change.
I do not want to initiate geolocation.watchPosition on mount inside useEffect.
I tried wrapping the callbacks with useCallback and passing the state value to the dependency array, but the callback still closes over the stale state value even though it is updated. Is there any workaround to this?
Simplified example:
function Map() {
const watchRef = useRef();
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
const success = useCallback((pos) => {
if(isBool) {
// do something...
return;
}
// do something else...
}, [isBool])
function updateStateHandler() {
setIsBool(true);
}
return // jsx...
}
Figured out a solution using refs to break out of the closure.
Solution:
function Map() {
const watchRef = useRef();
const isBoolRef = useRef(); // create a ref
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
// no need for useCallback
const success = (pos) => {
if(isBoolRef.current) {
// do something...
return;
}
// do something else...
}
function updateStateHandler() {
setIsBool(true);
}
useEffect(() => {
isBoolRef.current = isBool; // this will return the updated value inside the callback
}, [isBool])
return // jsx...
}
I am using a function (getActorInfo()) in react to grab info from an api and set that in a State. It works but the function wont stop running.
export default function ActorProfile({ name, img, list, id, getActorInfo }) {
const [showList, setShowList] = useState(false);
const [actorInfo, setActorInfo] = useState({});
getActorInfo(id).then(val => setActorInfo(val));
console.log(actorInfo)
return (
<Wrapper>
<Actor
id={id}
name={name}
img={img}
onClick={() => {
setShowList(!showList);
}}
actorBirthday={actorInfo.actorBirthday}
/>
{showList && <MovieList list={list} actorInfo={actorInfo} />}
</Wrapper>
);
}
I tried using useEffect like this
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, {});
But I get an error that I do not understand
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in ActorProfile (at App.js:60)
My question is how to have this function only run once?
Anything in a functional component body will run every render. Changing to a useEffect is the correct solution to this problem.
It isn't working for you because useEffect takes an array as its second parameter, not an object. Change it to [], and it will only run once.
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, []);
This will be equivalent to the class-based componentDidMount.
If your hook has a dependency, you add it to the array. Then the effect will check to see if anything in your dependency array has changed, and only run the hook if it has.
useEffect(() => {
// You may want to check that id is truthy first
if (id) {
getActorInfo(id).then(val => setActorInfo(val));
}
}, [id]);
The resulting effect will be run anytime id changes, and will only call getActorInfo if id is truthy. This is an equivalent to the class-based componentDidMount and componentDidUpdate.
You can read more about the useEffect hook here.
You are still not checking if the component is mounted before you set the state. You can use a custom hook for that:
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
Then in your component you can do:
const isMounted = useIsMounted();
useEffect(() => {
getActorInfo(id).then(
val => isMounted && setActorInfo(val)
);
}, [getActorInfo, id, isMounted]);
you need to cleanup useEffect like
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
return () => {
setActorInfo({});
}
},[]);
have a look at this article. It explains you why to cleanup useEffect.
I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in the useCallback dependency array will trigger the useEffect to run.
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls.
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}
I am using ReactHooks. I am trying to access ref of User component in useEffect function, but I am getting elRef.current value as null, though I passed elRef.current as second argument to useEffect. I am supposed to get reference to an element, but outside (function body) of useEffect, ref value is available. Why is that ? How can I get elRef.current value inside useEffect?
code
import React, { Component, useState, useRef, useEffect } from "react";
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(
() => {
setIsLoading(true);
fetch(url)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
setIsLoading(false);
setData(json.data);
})
.catch(error => {
setIsLoading(false);
setError(error);
});
},
[url]
);
return { data, isLoading, error };
};
const User = ({ id }) => {
const elRef = useRef(null);
const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};
class App extends Component {
state = {
userId: 1
};
handleNextClick = () => {
this.setState(prevState => ({
userId: prevState.userId + 1
}));
};
handlePrevNext = () => {
this.setState(prevState => ({
userId: prevState.userId - 1
}));
};
render() {
return (
<div>
<button
onClick={() => this.handlePrevClick()}
disabled={this.state.userId === 1}
>
Prevoius
</button>
<button onClick={() => this.handleNextClick()}>Next</button>
<User id={this.state.userId} />
</div>
);
}
}
export default App;
Codesandbox link
Thanks !
You should use useCallback instead of useRef as suggested in the reactjs docs.
React will call that callback whenever the ref gets attached to a different node.
Replace this:
const elRef = useRef(null);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
with this:
const elRef = useCallback(node => {
if (node !== null) {
console.log("ref", node); // node = elRef.current
}
}, []);
It's a predictable behaviour.
As mentioned #estus you faced with this because first time when it's called on componentDidMount you're getting null (initial value) and get's updated only once on next elRef changing because, actually, reference still being the same.
If you need to reflect on every user change, you should pass [user] as second argument to function to make sure useEffect fired when user is changed.
Here is updated sandbox.
Hope it helped.
When you use a function as a ref, it is called with the instance when it is ready. So the easiest way to make the ref observable is to use useState instead of useRef:
const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;
Then you can use it in dependency arrays for other hooks, just like any other const value:
useEffect(() => {
if (element) console.log(element);
}, [element]);
See also How to rerender when refs change.
useEffect is used as both componentDidMount and componentDidUpdate,
at the time of component mount you added a condition:
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
because of the above condition at the time of mount, you don't have the user, so it returns null and div is not mounted in the DOM in which you are adding ref, so inside useEffect you are not getting elRef's current value as it is not rendered.
And on the click of next as the div is mounted in the dom you got the value of elRef.current.
The assumption here is that useEffect needs to detect changes to ref.current, so needs to have the ref or ref.currentin the dependencies list. I think this is due to es-lint being a bit over-pedantic.
Actually, the whole point of useEffect is that it guarantees not to run until the rendering is complete and the DOM is ready to go. That is how it handles side-effects.
So by the time useEffect is executed, we can be sure that elRef.current is set.
The problem with your code is that you don't run the renderer with <div ref={elRef}...> until after user is populated. So the DOM node you want elRef to reference doesn't yet exist. That is why you get the null logging - nothing to do with dependencies.
BTW: one possible alternative is to populate the div inside the effect hook:
useEffect(
() => {
if(!user) return;
elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
}, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.
That way the if (!user) return null; line in the User component is unnecessary. Remove it, and elRef.current is guaranteed to be populated with the div node from the very beginning.
set a useEffect on the elem's.current:
let elem = useRef();
useEffect(() => {
// ...
}, [elem.current]);