How can I cancel setInterval if this.props.firebase is not equal to null?
VerifyEmail = () => {
const { firebase } = this.props;
console.log(firebase);
if (firebase !== null) {
this.setState({ validCode: true, verifiedCode: true });
}
};
componentDidMount() {
const { firebase } = this.props;
if (firebase === null) {
this.intervalID = setInterval(() => this.VerifyEmail(), 1000);
} else if (firebase !== null) {
clearInterval(this.intervalID);
}
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
Currently, this.VerifyEmail is repeated and my attempt to clearInterval if this.props.firebase is not equal to null does not run.
Also, if you have another method that will automatically action this.VerifyEmail() on page render and when this.props.firebase is not equal to null. Please suggest.
componentDidMount(or cDM in short) runs just once when component is mounted. Later when you change props provided it is not called. Upon props is changed componentDidUpdate is run. You may place your check there keeping in mind to check prevProps.firebase !== this.props.firebase or you may stack in endless re-rendering. Just like that
componentDidUpdate(prevProps) {
const { firebase: prevFirebase } = this.prevProps;
const { firebase } = this.props;
if (!prevFirebase && firebase) { // null => not-null
this.intervalID = setInterval(() => this.VerifyEmail(), 1000);
}
if (prevFirebase && !firebase) { // not-null => null
clearInterval(this.intervalID);
}
}
Don't miss placing the same logic in componentDidMount(for case when props.firebase has been provided initially) and componentDidUpdate(for case when props.firebase is initially null and later is assigned to props).
But I believe there could be more DRY solution. Do you really need to render this component until data is retrieved? Do you render some placeholder until firebase appears and data is loaded? If yes, then how about lift up data loading to parent component? Then your component would retrieve data through props and it might have render as simple as (this.props.data && ...)
Related
I have simple blog with articles. And I want to rewrite classes to functional components and hooks.
Now I got this logic in lifecycle methods for my page with edit/add form:
it works fine.
componentDidUpdate(prevProps, prevState) {
if (this.props.match.params.id !== prevProps.match.params.id) {
if (prevProps.match.params.id) {
this.props.onUnload();
}
this.id = this.props.match.params.id;
if (this.id) {
return this.props.onLoad(userService.articles.get(this.id));
}
this.props.onLoad(null);
}
this.isLoading = false;
}
componentDidMount() {
if (this.id) {
this.isLoading = true;
return this.props.onLoad(userService.articles.get(this.id));
}
this.isLoading = false;
this.props.onLoad(null);
}
componentWillUnmount() {
this.props.onUnload();
}
shouldComponentUpdate(newProps, newState) {
if (this.props.match.params.id !== newProps.match.params.id) {
this.isLoading = true;
}
return true;
}
I rewrote it all to hooks like that:
//componentDidMount
useEffect(() => {
if (id) {
setIsloading(true);
return props.onLoad(userService.articles.get(id));
}
setIsloading(false);
props.onLoad(null);
}, []);
useEffect(()=> {
prevId.current = id;
}, [id]
);
//componentDidUpdate
useEffect(() => {
//const id = props.match.params.id;
if (id !== prevId.current) {
if (prevId.current) {
props.onUnload();
}
if (id) {
return props.onLoad(userService.articles.get(id));
}
props.onLoad(null);
}
setIsloading(false);
});
//componentWillUnmount
useEffect(() => {
return props.onUnload();
}, []);
I got error - "Too many re-renders." at codesandbox
full code: codesandbox
Its strange, but at localhost there is no error "Too many re-renders."
Don't know what to do with my class "shouldComponentUpdate" method how to rewrite it to hooks. Tryed 'memo' but have no idea how to write in in this case.
And anyway I'm missing something, because it all won't work - it's not updating form fields properly.
If you have a good knowledge with react hooks please help, or give some advice - how to fix it?
The effect without dependency is causing "Too many re-renders.": it runs after every render then it calls setIsLoading to update the state( loading) which will cause the component to re-render, which will run the effect again and the setState will be called again and effect and so on...
//componentDidUpdate
useEffect(() => {
//const id = props.match.params.id;
if (id !== prevId.current) {
if (prevId.current) {
props.onUnload();
}
if (id) {
return props.onLoad(userService.articles.get(id));
}
props.onLoad(null);
}
setIsloading(false);
})
to fix the issue either remove the setIsLoading from it, or add IsLoading as dependency.
//componentDidUpdate
useEffect(() => {
...
setIsloading(false);
},[isLoading]);
you can also merge two mount effects into one like this ( yours is also working, but I think this is stylistically preferable):
//componentDidMount
useEffect(() => {
if (id) {
setIsloading(true);
return props.onLoad(userService.articles.get(id));
}
setIsloading(false);
props.onLoad(null);
//componentWillUnmount
return function () {
props.onUnload()
}
}, []);
for the second bullet: about rewriting your component's shouldComponentUpdate; first I must point that your existing shouldComponentUpdate isn't sound reasonable, since you always return true, and you are only using it to trigger loading state; and its not eligible to be written with React.memo (which is ~equivalent of shouldComponentUpdate in class components); so you only need something to be executed on every props change to determine loading state and for that you can use an effect with props as its dependency like this:
//manually keeping old props id
const prevPropsId = useRef();
useEffect(() => {
if (prevPropsId.current !== props.match.params.id) {
setIsLoading(true);
}
prevPropsId.current = props.match.params.id;
}, [props.match.params.id]);
// this hook only run if `props.match.params.id` change
based on my realization this shall do the job, ( despite having hard time understanding why you write the logic this way in the first place) and if it's not doing fine you may tune the logic a little bit to match your needs, you get the idea how the props change effect works. also you many need to handle typeError in case id, params or match doesn't exist and
Optional chaining can be handy tool here -> props.match?.params?.id
If you miss the old lifecycle hooks, you could always recreate them as custom hooks:
function useComponentDidMount(effect) {
useEffect(() => {
effect();
}, []);
}
function useComponentWillUnmount(effect) {
useEffect(() => {
return effect;
}, []);
}
function useComponentDidUpdate(effect, dependencies) {
const hasMountedRef = useRef(false);
const prevDependenciesRef = useRef(null)
useEffect(() => {
// don't run on first render, only on subsequent changes
if (!hasMountedRef.current) {
hasMountedRef.current = true;
prevDependenciesRef.current = dependencies;
return;
}
// run effect with previous dependencies, like in componentDidUpdate!
effect(...prevDependenciesRef.current || []);
prevDependenciesRef.current = dependencies;
}, dependencies)
}
Usage (your example refactored):
//componentDidMount
useComponentDidMount(() => {
if (id) {
setIsloading(true);
props.onLoad(userService.articles.get(id));
return;
}
setIsloading(false);
props.onLoad(null);
});
//componentDidUpdate
useComponentDidUpdate((prevId) => {
if (prevId) {
props.onUnload();
}
if (id) {
props.onLoad(userService.articles.get(id));
return;
}
props.onLoad(null);
setIsloading(false);
}, [id]);
//componentWillUnmount
useComponentWillUnmount(() => {
props.onUnload();
});
If someone can help me with this ?
I need this function to store the filteredObject key in the state. but when I call this function in componentDidMount(), it didn't work and when I called it in ComponentDidUpdate() it works but going on a infinite loop?
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => { return agent.email === returnedEmail })
if (filteredEmail) {
this.setState({
agentApplicationId: filteredEmail.properties
})
}
}
}
You need to be very careful when setting state in componentDidUpdate. Calling setState updates the component, which triggers componentDidUpdate, which calls setState, and so on, causing the infinite loop. From the React docs:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition...or you’ll cause an infinite loop.
The solution is to add some kind of condition so you're not updating state unnecessarily. For example:
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents, agentApplicationId } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => agent.email === returnedEmail);
// Add an extra condition here to prevent state from updating if the values are already equal.
if (filteredEmail && filteredEmail.properties !== agentApplicationId) {
this.setState({
agentApplicationId: filteredEmail.properties
});
}
}
}
Am trying to reset the state value after some time using setTimeout function.
const SetTimeout = props => {
let [errorText, setError] = useState(
props.errorMessage ? props.errorMessage : ""
);
useEffect(() => {
if (errorText !== "") {
setTimeout(() => {
setError("");
}, 4000);
} else {
// console.log("no error")
}
}, [errorText]);
return <div>{errorText}</div>;
};
const MapStateToProps = state => {
return {
errorMessage: state.errorReducer.error
};
};
Am getting the errorMessage from an api call, after some time i'm trying to make that message to empty using useState.
But the component is getting re-rendered with the same error message. could you please help me in fixing this?
You are mixing component state and redux state here which is pretty bad and why you are seeing this behaviour.
You are initially rendering the component with the error message from within Redux, and passing this into the component as a property value. The component displays this, set's it's own state value. After the timeout runs, it's updating it's own state, which causes a re-render, and the Redux value is again passed into the component via the properties.
I would suggest you forget about using useState and use a Redux action to clear the error message that it is storing.
you are using redux to not use state in react components and here you are using state in your component. so what you can do is to add the erromessage to your global state :
state ={
...,
errormessage:undefined
}
and then in your action you need to change your state.errormessage like this:
export const show_error = message => {
return {
type: "SHOW_ERROR",
payload: message
};
};
export const remove_error = () => {
return {
type:"REMOVE_ERROR"
};
};
imagine calling post api to add post using redux thunk you can do it like this:
export function add_post(data) {
return function(dispatch) {
return axios
.post('/posts', data)
.then(res => {
dispatch(fetch_posts_data());})
.catch(err => dispatch(
show_error(err)
);
setTimeout(function() {
dispatch(remove_error());
}, 2000););
};
}
in your reducer :
case "SHOW_ERROR":
return {
...state,
posts: { ...state.posts, errormessage: action.message}
};
case "REMOVE_ERROR":
return {
...state,
posts: { ...state.posts, errormessage: undefined }
};
now to retrieve the errormessage in your component you can use normal connect method but here i will use redux hooks useSelector():
const Error = ()=>{
const errormessage = useSelector(state=>state.errormessage)
return(
<>
{
errormessage?
<div>{errormessage}</div>
:""
}
</>
)
}
I've just found this out! So it may not be perfect.
Hooks have a useSelector and a useDispatch (from react-redux)
To stick with having your error in Redux, you component would look like this:
const SetTimeout = () => {
const errorText = useSelector(state => state.errorReducer.error);
const clearError = useDispatch();
useEffect(() => {
if (errorText !== "") {
const timeout = setTimeout(() => {
clearError({ type: "CLEAR_ERROR" });
}, 5000);
} else {
// console.log("no error")
}
// this will clear Timeout when component unmounts like in willComponentUnmount
return () => { clearTimeout(timeout); };
}, []);
return <div style={{ color: "red" }}>{errorText}</div>;
};
export default SetTimeout;
Pretty cool functional component without the redux boiler plate code.
Hope that helps
I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}
Gutentag, guys!
I keep getting this error message from my application after unmounting the component:
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
in Header (at index.js:27)
Now here is the code from the Header component:
class Header extends Component {
isCancelled = false;
state = {
someStateVars: x,
separateColumns: 'true',
}
handleChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
if (!this.isCancelled) {
this.setState({ //######THIS IS LINE 27######
[name]: value
});
}
}
handleDisplayChange = (event) => {
const value = event.target.value;
const name = 'separateColumns';
if (!this.isCancelled) {
this.setState({
[name]: value
}, () => {
this.props.displayChange();
});
}
}
serversRefresh = () => {
if (!this.isCancelled) {
setTimeout(() => {
this.setState({refreshed: false});
}, localStorage.getItem('seconds')*1000); //disable refresh for 15 seconds
}
}
reactivateButton = () => {
if (!this.isCancelled) this.setState({refreshed: false});
}
componentDidMount() {
if(localStorage.getItem('seconds')>5 && !this.isCancelled){
this.setState({refreshed: true});
}
}
componentWillUnmount() {
this.isCancelled = true;
}
}
When I saw I was getting this error, I've added isCancelled variable which is changed in componentWillUnmount() function to true.
After I unmount the Header component, after 15 seconds, when the serversRefreshbutton is reactivated, I get this error message.
How can I fix it?
On another component where I've encountered this issue "isCancelled" var did help, but here I see it has no impact and the issue persists.
Just store your timeout in a variable, e.g.
this.timeout = setTimeout(/* your actions here*/, /* your timeout */)
and then clear your timeout in the componentWillUnmount
componentWillUnmount() {
clearTimeout(this.timeout)
}
It should fix your problem without crutches like this.isCancelled. Detecting component's mount state is no-op, because it still unloaded from memory even after unmounting.
setTimeout returns timer's id with which it can be cancelled in future. clearTimeout cancels timeout by it's id, if it is not yet executed.
More about your case you can read here: Why isMounted is antipattern.
More about timers on MDN.