I am going to count loaded apis. I used function component.
function Screen(props) {
const [count, setCount] = useState(0)
useEffect(() => {
loadData();
() => { return null; }
}, [])
const loadData = () => {
axios({ url: API_BASE_URL + 'countries', method: 'get' }).then((res)=>{
setCount(count+1)
})
axios({ url: API_BASE_URL + 'regions', method: 'get' }).then((res)=>{
setCount(count+1)
})
}
useEffect(() => {
console.info(count) // this shows 0, 1, it never be 2
}, [count])
return <div></div>
}
what's wrong with my code? I think it should print 2 at last
Thanks
count is captured as 0 by the closures you pass to then. Effectively you're calling setCount(0 + 1). A very thorough explanation can be found here: https://overreacted.io/a-complete-guide-to-useeffect/
Here's an updated version of your code, that fixes the issues:
import React, { useCallback, useEffect, useState } from 'react';
const fakeApi = (delay = 400, val = null): Promise<typeof val> => new Promise(resolve => {
setTimeout(resolve, delay, val);
});
const Count: React.FC = () => {
const [count, setCount] = useState(0);
const loadData = useCallback(() => {
fakeApi(1000).then(() => {
setCount(count => count + 1); // use the function form of setState to get the current state value without being *dependent* on it.
});
fakeApi(2000).then(() => {
setCount(count => count + 1); // same as above.
});
}, []); // no dependencies => loadData is constant
useEffect(() => {
loadData();
}, [loadData]); // dependent on loadData, but that changes only once (first render, see above).
// note: you could move loadData inside the effect to reduce some code noise. Example:
//
// useEffect(() => {
// const loadData = () => {
// fakeApi(1000).then(() => {
// setCount(count => count + 1);
// });
//
// fakeApi(2000).then(() => {
// setCount(count => count + 1);
// });
// };
//
// loadData();
// }, []);
return (
<div>{count}</div>
);
};
export default Count;
Demo: https://codesandbox.io/s/focused-sun-0cz6u?file=/src/App.tsx
Related
In the following React Component below, I am trying to add increment count by each second passed so it looks like a stopwatch, but the count is shown as 2, then blinks to 3, and back to 2. Does anyone know how to deal with this bug, and get the count to show up as intended?
import React, { useEffect, useState } from "react";
const IntervalHook = () => {
const [count, setCount] = useState(0);
const tick = () => {
setCount(count + 1);
};
useEffect(() => {
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [ count ]);
return <h1> {count} </h1>;
};
export default IntervalHook;
if you want to change some state based on its previous value, use a function:setCount(count => count + 1); and your useEffect becomes independant of [ count ]. Like
useEffect(() => {
const tick = () => {
setCount(count => count + 1);
};
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [setCount]);
or you get rid of tick() and write.
useEffect(() => {
const interval = setInterval(setCount, 1000, count => count + 1);
return () => {
clearInterval(interval);
};
}, [setCount]);
But imo it's cleaner to use a reducer:
const [count, increment] = useReducer(count => count + 1, 0);
useEffect(() => {
const interval = setInterval(increment, 1000);
return () => {
clearInterval(interval);
};
}, [increment]);
That's the warning in the console,
Warning: 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.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
I have this code which updates the state count every 1 seconds.
How can I access the value of the state object in setInterval() ?
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
export default function Timer({objectId}) {
const [object, setObject] = useState({increment: 1});
const [count, setCount] = useState(0);
useEffect(() => {
callAPI(); // update state.object.increment
const timer = setInterval(() => {
setCount(count => count + object.increment); // update state.count with state.object.increment
}, 1000);
return () => clearTimeout(timer); // Help to eliminate the potential of stacking timeouts and causing an error
}, [objectId]); // ensure this calls only once the API
const callAPI = async () => {
return await axios
.get(`/get-object/${objectId}`)
.then(response => {
setObject(response.data);
})
};
return (
<div>{count}</div>
)
}
The only solution I found is this :
// Only this seems to work
const timer = setInterval(() => {
let increment = null;
setObject(object => { increment=object.increment; return object;}); // huge hack to get the value of the 2nd state
setCount(count => count + increment);
}, 1000);
In your interval you have closures on object.increment, you should use useRef instead:
const objectRef = useRef({ increment: 1 });
useEffect(() => {
const callAPI = async () => {
return await axios.get(`/get-object/${objectId}`).then((response) => {
objectRef.current.increment = response.data;
});
};
callAPI();
const timer = setInterval(() => {
setCount((count) => count + objectRef.current);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [objectId]);
Is it safe to do something like the following?
const [foo, setFoo] = useState(undefined)
useEffect(() => {
dispatch(someFunc()).then(response => {
let { someFoo } = response
setFoo(someFoo)
})
}, []) // or }, [bar])
useEffect(() => {
dispatch(anotherFunc()).then(response => {
let { anotherFoo } = response
setFoo(anotherFoo)
})
}, [bar])
The effects are executed in the given order and only the "foo" from the last effect setter will be visible in the UI. For instance, the following component will output bar - 1:
const Component = ({ bar }) => {
const [foo, setFoo] = useState(undefined);
console.log("render", bar, foo);
useEffect(() => {
console.log("effect 1");
let someFoo = bar + 1;
setFoo(someFoo);
}, [bar]);
useEffect(() => {
console.log("effect 2");
let anotherFoo = bar - 1;
setFoo(anotherFoo);
}, [bar]);
return (
<div>
{bar} sets {foo}
</div>
);
};
https://codesandbox.io/s/proud-leaf-g55n9?file=/src/App.js:76-482
EDIT: If you use [] as the dependency array, it will only execute once. If there's an async function inside the effect like fetch, the last executed setFoo will prevail. The following example will display random results in each click:
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(1);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(2);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
Example 2:
https://codesandbox.io/s/jovial-architecture-0ybcz?file=/src/App.js
I am trying to create a reusable hook that solves the problem of the stale closure problem that is outlined in this blog post.
Here is a codesandbox that shows the problem of the stale closure in action:
const useInterval = (callback, delay) => {
useEffect(() => {
let id = setInterval(() => {
callback();
}, 1000);
return () => clearInterval(id);
}, []);
};
const App: React.FC = () => {
let [count, setCount] = useState(0);
useInterval(() => setCount(count + 1), 1000);
return <h1>{count}</h1>;
};
Basically count is frozen at 0 when the closure is created meaning 0 is added to 1 continually in the setInterval.
The blog post solves this by introducing a mutable ref to store the callback in:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
The useEffect never gets re-executed because the callback is not in the useEffect dependency array with the setInteval.
I've seen some libraries using a hook like the useStoreCallback below but the linter still complains that the savedCallback variable below needs to be added to the dependency array.
Is this actually better?
type UnknownResult = unknown;
type UnknownArgs = any[];
function useStoreCallback<R = UnknownResult, Args extends any[] = UnknownArgs>(
fn: (...args: Args) => R
) {
const ref = React.useRef(fn);
useEffect(() => {
ref.current = fn;
});
return React.useCallback<typeof fn>(
(...args) => ref.current.apply(void 0, args),
[]
);
}
function useInterval<R = UnknownResult, Args extends any[] = UnknownArgs>(
callback: (...args: UnknownArgs) => R,
delay: number
) {
const savedCallback = useStoreCallback(callback);
useEffect(() => {
function tick() {
savedCallback();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay, savedCallback]);
}
const App: React.FC = () => {
let [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
};
Use the functional setState and rid off the closure.
const App: React.FC = () => {
const [count, setCount] = useState(0);
useInterval(() => setCount(c => c + 1), 1000);
return <h1>{count}</h1>;
};