There is a simple code where i make the api call with react redux.
But there is one more thing. periodically i increase the progress value I show on the screen. I use useEffect for this. but when i increase progress, api goes back to call. I just want to make my api call once.
here is an example of my code
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
dispatch(myApiCall);
useEffect(() => {
const interval = setInterval(() => {
setProgress(progress => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, [progress]);
return (
<ProgressBar
completed={progress}
/>
);
};
You only need to call the API in useEffect too:
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
useEffect(() => {
dispatch(myApiCall);
}, [dispatch]);
useEffect(() => {
const interval = setInterval(() => {
setProgress((progress) => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, []);
return <ProgressBar completed={progress} />;
};
You need to call your api call inside of a componentDidMount equivalent useEffect:
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
useEffect(() => {
dispatch(myApiCall);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setProgress(progress => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, [progress]);
return (
<ProgressBar
completed={progress}
/>
);
};
Related
i've implemented infinite scroll hook for my newspage
const [posts, setPosts] = useState([]);
const [currentOffset, setCurrentOffset] = useState(0);
const [isLoading, setLoading] = useState(true);
const [isFetching, setIsFetching] = useState(false);
let loadThreePosts = () => {
axios.get(`/news?limit=3&offset=${currentOffset}`).then(({ data }) => {
let threePosts = [];
console.log(data);
data.data.forEach((p) => threePosts.push(p));
setPosts((posts) => [...posts, ...threePosts]);
setLoading(false);
});
setCurrentOffset(currentOffset + 3);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
if (!isFetching) return;
fetchMorePosts();
}, [isFetching]);
function handleScroll() {
if (
window.innerHeight + document.documentElement.scrollTop !==
document.documentElement.offsetHeight ||
isFetching
)
return;
setIsFetching(true);
}
function fetchMorePosts() {
setTimeout(() => {
loadThreePosts();
setIsFetching(false);
}, 2000);
}
So if i go to the newspage throw the Link on my site it fetching posts as i need,
but if i paste newspage link in browser address bar it's not fetch anything
Change your code to this:
useEffect(() => {
window.addEventListener("scroll", handleScroll);
handleScroll(); // Call handleScroll function
return () => window.removeEventListener("scroll", handleScroll);
}, []);
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]);
I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
APP PART:
useInterval(() => {
getMessage();
}, 10000)
const getMessage = async () => {
setProcessing(true)
try {
const res = await fetch('url')
const response = await res.json();
setRecievedData(response)
}
catch (e) {
console.log(e)
}
finally {
setProcessing(false)
}
}
const getMessageManually = () => {
getMessage()
RESTART INTERVAL
}
You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.
Here is the code for the hook which can be reset and stopped.
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
const intervalRef = useRef(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(savedCallback.current, delay);
intervalRef.current = id;
return () => clearInterval(id);
}
}, [delay]);
useEffect(()=>{
// clear interval on when component gets removed to avoid memory leaks
return () => clearInterval(intervalRef.current);
},[])
const reset = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
intervalRef.current = setInterval(savedCallback.current,delay)
}
});
const stop = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
}
})
return {
reset,
stop
};
};
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();
You should add a reset function as returning a value from the hook.
I also fixed few issues and added an unmount handler:
// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval();
// Implementation
const useInterval = (callback, delay) => {
const savedCallbackRef = useRef(callback);
const intervalIdRef = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// handle tick
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
const resetInterval = useCallback(() => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(savedCallback.current, delay)
}, [delay]);
return resetInterval;
};
Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback
so updating the above solution
// Implementation
const useInterval = (callback, delay) => {
const intervalIdRef = useRef();
// handle tick
useEffect(() => {
const tick = () => {
callback();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
};
And then you can use it like this
const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
setCounter(counter + 1)
// setCounter will reset the interval
}, [counter])
useResetInterval(() => {
onTimerFinish()
}, 5000)
I recently used hooks with React to fetch data from server but i'm facing a problem with hooks. The code seems correct but it look like the useEffect isn't called at first time but 3 seconds after with the setInterval. I have blank table for 3 seconds before it appear. I want to directly show the data and call it 3 seconds later.
What is the correct way to use it ?
const [datas, setDatas] = useState([] as any);
useEffect(() => {
const id = setInterval(() => {
const fetchData = async () => {
try {
const res = await fetch(URL);
const json = await res.json();
setDatas(jsonData(json));
} catch (error) {
console.log(error);
}
};
fetchData();
}, TIME)
return () => clearInterval(id);
}, [])
You need to invoke fetchData once initially outside the interval. Define fetchData outside the interval.
useEffect(() => {
// (1) define within effect callback scope
const fetchData = async () => {
try {
const res = await fetch(URL);
const json = await res.json();
setDatas(jsonData(json));
} catch (error) {
console.log(error);
}
};
const id = setInterval(() => {
fetchData(); // <-- (3) invoke in interval callback
}, TIME);
fetchData(); // <-- (2) invoke on mount
return () => clearInterval(id);
}, [])
With React Hooks:
const [seconds, setSeconds] = useState(0)
const interval = useRef(null)
useEffect(() => { if (seconds === 60) stopCounter() }, [seconds])
const startCounter = () => interval.current = setInterval(() => {
setSeconds(prevState => prevState + 1)
}, 1000)
const stopCounter = () => clearInterval(interval.current)
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>;
};