I have a TimeLine component that renders posts 5 at a time
and when the user scrolls to the end of the page the throttle function is called.
I am trying to throttle the request to a maximum of once per second but The API is called multiple times
I tried several solutions like:
making a global flag that won't let you use the function unless a certain timeout has passed "It doesn't make sense because I am using the throttle to do that but I tried to check if my implementation is wrong"
declaring the throttle function alone outside the component
TimeLine.js
export default function TimeLine() {
const dispatch = useDispatch();
const posts = useSelector(state => state.postReducer.posts);
const loaded = useSelector(state => state.postReducer.loaded);
const TIMEOUT = 1000;
// Loaded keeps track of loaded posts
if(loaded === 0){
API.Post.getAllPosts(loaded)(dispatch);
}
if(loaded > 5){
if(loaded % 5 === 0) window.scrollTo(0, document.body.scrollHeight * 0.6)
}
window.addEventListener('scroll', () => throttledReq(loaded, TIMEOUT, dispatch));
const classes = useStyle();
return (
<div>
<NewPost />
{(posts && posts.length > 0)?posts.map((post, index) => (
<div key={post._id} className={classes.post}>
<Post
currentIndex={index}
{...post.user}
dispatch={dispatch}
postID={post._id}
postContent={post.text}
/>
</div>
)): false}
</div>
);
};
the Throttle function
const throttledReq = (loaded, TIMEOUT, dispatch) => _.throttle(e => {
const bottomLimit = document.documentElement.offsetHeight - window.innerHeight;
if(document.documentElement.scrollTop === bottomLimit){
API.Post.getAllPosts(loaded)(dispatch);
}
}, TIMEOUT)();
My requests
Every time you scroll, you fire dispatch, which in turn re-runs the component, adding another event listener. This will continue infinitely.
You'll only want the event listener to be added the first time the component is rendered. You can do this with the useEffect hook.
useEffect(() => {
window.addEventListener('scroll', () => throttledReq(loaded, TIMEOUT, dispatch));
}, []);
The empty array at the end means that it doesn't have any dependencies, so it will only run once whenever the component is rendered. Every rerender won't call the callback.
Your loaded value should use a useEffect as well, but in this case with the loaded variable as a dependency. Every time loaded is changed (and on the first render), the callback is fired and the logic assessed.
useEffect(() => {
if (loaded === 0) {
API.Post.getAllPosts(loaded)(dispatch);
}
if (loaded % 5 === 0) {
window.scrollTo(0, document.body.scrollHeight * 0.6)
}
), [loaded]);
Since you're trying to call a certain logic whenever a scroll position is reached, or some element is in view, I recommend you take a look at the Intersection Observer API which eliminates the requirement of a throttled onscroll function.
Related
I'm reading this article and I'm not sure I understand how the final hook works.
Here is the code:
const useAnimationFrame = (callback) => {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
}
and used for example in this way:
const [count, setCount] = useState(0);
useAnimationFrame((deltaTime) => {
setCount((prevCount) => {
return prevCount + 1;
});
});
Ok, the goal is have a number value that is incremented every frame.
I can explain what is happens running this code:
the component create a local state with useState(0)
then the useAnimationFrame hook is called using this callback as parameter:
(deltaTime) => {
setCount((prevCount) => {
return prevCount + 1;
});
}
the function takes a number as input and increment ste state value of one each time it is called.
useAnimationFrame is a function that takes another function as a parameter (a callback). It creates two refs. At the first time it is executed (because of the []) it calls the useEffect. It saves in requestRef.current the timestamp the requestAnimationFrame returns. The requestRef.current calls the animate function that computes the delta time between the request animation frames (the previous and the current) and then call the callback with this value so it calls the setCount. Then it updates the current refs values and recall the requestAnimationFrame.
So the cycle should be:
component
> count = 0
useAnimationFrame <--------------+
> requestRef = ? |
> previousTimeRef = ? |
useEffect |
animate |
> deltaTime = delta#1 |
> count = 1 |
> previousTimeRef.current = time#1 |
> requestRef.current = time#2 -------+
> requestRef.current = timestamp#1
Am I wrong?
It might be helpful to track the function signatures of requestAnimationFrame and cancelAnimationFrame.
requestAnimationFrame takes a single argument, a callback function. The callback function itself receives a single timestamp argument (DOMHighResTimeStamp)
cancelAnimationFrame takes a single argument, the id of the requestAnimationFrame which you want to cancel.
So time in the animate callback function is the single argument received via the api, a DOMHighResTimeStamp similar to the one returned by performance.now(), indicating the point in time when requestAnimationFrame() starts to execute callback functions.
const animate = (time) => {
This is a check to see if the hook has already run 1x. If it has, update the parent React scope with the new time minus the previous time
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime);
}
Once the hook is confirmed as having run, save the DOMHighResTimeStamp for future calculations
previousTimeRef.current = time;
After this, it gets a bit interesting and I am not sure this is best approach. It may even be a bug. The code sets a new listener and updates the ref with the latest id, based on the result of a new invocation.
Just from reading the code, I am not sure the original listener ever gets cancelled. I suspect it is not.
/// this is an id
requestRef.current = requestAnimationFrame(animate);
I don't have access to a running version, but I would suggest removing the requestRef.current altogether and see if the clean-up happens as expected when the useEffect clean-up happens e.g.
useEffect(() => {
const id = requestAnimationFrame(animate);
return () => cancelAnimationFrame(id);
}, []);
This will also simplify the embedded refs as well to make reading a bit more clear.
I am trying to capture the no of clicks by the user.
Which i wish to send to an api every 15 min,
I am useing setinterval inside useEffect to achive this, but the problem is even though the state is changing outside but not inside setinterval. setinterval is only giving the initial default value.
here is the code -
const Agent = (props: RouteComponentProps) => {
const [clicks, setClicks] = useState(0);
const handleOnIdle = () => {
console.log("user is idle");
console.log("last active", new Date(getLastActiveTime()));
console.log("total idle time: ", getTotalIdleTime() / 1000);
};
const handleOnActive = () => {
console.log("user is active");
console.log("time remaining", getRemainingTime());
};
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks + 1);
};
const {
getRemainingTime,
getLastActiveTime,
getTotalIdleTime,
} = useIdleTimer({
timeout: 10000,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500,
});
useEffect(() => {
let timer = setInterval(
() => alert(`user clicked ${clicks} times`),
1000 * 30
);
return () => {
clearInterval(timer);
setClicks(0);
};
}, []);
return (
<AgentLayout>
<div className="dashboard-wrapper py-3">
<Switch>
<Redirect
exact
from={`${props.match.url}/`}
to={`${props.match.url}/home`}
/>
<Route path={`${props.match.url}/home`} component={Home} />
<Route
path={`${props.match.url}/lead-details`}
component={leadDetails}
/>
<Route
path={`${props.match.url}/fill-details`}
component={FillDetails}
/>
<Route
path={`${props.match.url}/my-desktime`}
component={MyDesktime}
/>
<Redirect to="/error" />
</Switch>
</div>
</AgentLayout>
the alert is giving user clicked 0 times
The problem is that only the first version of clicks is used by the timer function, because it closes over the version as of the first time your component function is called. When you pass an empty dependency array to useEffect, the useEffect callback is only called once, the first time the component function is called (when mounting the component).
You can fix that by having the timer function use the setClicks method instead:
useEffect(() => {
let timer = setInterval(
() => {
setClicks(currentClicks => { // ***
alert(`user clicked ${currentClicks} times`); // ***
return currentClicks; // ***
}); // ***
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the component is unmounting
};
}, []);
Now, the timer calls the setClicks using the callback form, which means it receives the current value of clicks. Because it returns that same value, it doesn't update the state of the component.
It's also possible to solve this by adding clicks as a dependency to the useEffect, but it's a bit complicated. The naive way would be just to do this:
// The naive way
useEffect(() => {
let timer = setInterval(
() => {
alert(`user clicked ${clicks} times`);
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the user's click count would get reset every time
};
}, [clicks]);
// ^^^^^^−−−−−−−−−−−−− ***
That will mostly work, but it will restart the timer every time clicks changes, even if that means that instead of waiting 30 seconds, it waits 45 (because clicks changed after 15 seconds, which cancelled the previous interval and started it again). That probably wouldn't matter for a really short interval, but for a 30 second one it seems less than ideal. Doing it this way without messing up the timing of the interval requires that you remember when the next timer callback should have happened and adjusting the duration of the initial delay to match, which gets fairly complicated.
I didn't notice earlier, but any time you're updating state based on existing state, I recommend using the callback form. So your
const handleOnAction = () => {
setClicks(clicks => clicks + 1); // Note the function callback
};
Issues
You've a stale enclosure of the clicks state, closed over from the initial render cycle when the mounting effect ran and started the interval.
Your click updater should use a functional update to update the clicks count from the previous state. This covers if the user is somehow able to click faster then the react component lifecycle and enqueue more than one clicks update in a render cycle.
T.J.'s answer is good, but I consider the useState updater function to be a pure function and the alert(`user clicked ${currentClicks} times`); in the middle of it is a side-effect and (IMHO) should be avoided.
Solution
Update handleOnAction to use a functional state update.
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks => clicks + 1);
};
I suggest using a React ref and additional useEffect to update it, to hold a cached copy of the clicks state for the interval callback to reference.
const clicksRef = useRef();
useEffect(() => {
clicksRef.current = clicks; // update clicks ref when clicks updates
}, [clicks]);
useEffect(() => {
const timer = setInterval(
() => alert(`user clicked ${clicksRef.current} times`), // use clicks ref instead
1000 * 30
);
return () => {
clearInterval(timer);
// TJ already covered removing this extra state update
};
}, []);
I'm trying to create a simple idle game so that I can dive deeper into web development.
However, react is complaining about too many re-render, but I do want to re-render every second.
This is the code I have at the moment.
> const Game = () => { const [resourceCopper, setResourceCopper] =
> useState(0);
>
> const gatherCopper = () => {
> setResourceCopper(resourceCopper + 1); };
>
> setInterval(gatherCopper(), 1000);
>
> return (
> <section className="main-window-container">
> <section className="left-container">
> <h3>Resources:</h3>
> <p>Copper: {resourceCopper}</p>
> </section>
The immediate issue is that you're immediately executing gatherCopper, which immediately updates the state, rerender, and will cause an infinite loop.
You should remove the () behind gatherCopper in the setInterval call.
However, this code is very leaky, and because of the way React works you will create a new interval for every time the component renders. This will not work as expected.
The interval should be moved to a a React hook (i.e. useEffect), perhaps there's even hooks which wrap setInterval. A google search will probably come up with some good examples.
With React hooks you'll be able to start the interval when the component mounts for the first time, and you can also tell it to cancel the setInterval when the component unmounts. This is important to do.
update: Example with annotations (sandbox link)
const Game = () => {
const [resourceCopper, setResourceCopper] = useState(0);
useEffect(() => {
// We move this function in here, because we only need this function once
const gatherCopper = () => {
// We use the 2nd way of calling setState, which is a function that will
// receive the current value. This is so that we don't have to use the resourceCopper
// as a dependency for this effect (which would fire it more than once)
setResourceCopper(current => current + 1);
};
// We tell the setInterval to call the new gatherCopper function
const interval = setInterval(gatherCopper, 1000);
// When an effect returns a function, it will be used when the component
// is cleaned up (a.k.a. dismounts). We want to be neat and cancel up the interval
// so we don't keep calling this on components that are no longer there
return () => clearInterval(interval);
}, [setResourceCopper]);
return (
<section className="main-window-container">
<section className="left-container">
<h3>Resources:</h3>
<p>Copper: {resourceCopper}</p>
</section>
</section>
);
};
I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.
Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.
Here is my component code:
const Partners = (props: any) => {
const [query, setQuery] = useState("");
const { partners, totalPages, currentPage } = props.partnersList;
useEffect(() => {
props.getPartners();
}, []);
useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);
const handleQueryChange = (e: any) => {
setQuery(e.target.value);
};
const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
props.getPartners(query, value);
};
return (
<React.Fragment>
<Sidebar />
<MainContent className="iamSearchPartners">
<h1>Partners</h1>
<TextField
className="iamSearchPartners__search_input"
variant="outlined"
size="small"
onChange={handleQueryChange}
value={query}
/>
<PartnersList partners={partners} />
{totalPages > 1 && (
<Pagination
currentPage={currentPage || 1}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
)}{" "}
</MainContent>
</React.Fragment>
);
};
As you can see, I have a useEffect listening to changes to query.
debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.
To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.
const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);
const Partners = (props: any) => {
// omitted ...
useEffect(() => {
myDebouncedFunction(props.getPartners, query);
}, [query]);
// omitted ...
useMemo or putting it into a useState could work as well.
Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.
Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.
I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.
If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.
This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.
const timer = useRef<number>(0);
const lastEventSent = useRef(Date.now());
useEffect(() => {
if (Date.now() - lastEventSent.current < 500) {
// The event was fired too recently, but we still want
// to fire this event after the timeout if it is the last
// one (500ms in this case).
timer.current = setTimeout(() => {
lastEventSent.current = Date.now();
props.getPartners(query)
}, 500);
} else {
// An event hasn't been fired for 500ms, lets fire the event
props.getPartners(query)
lastEventSent.current = Date.now();
// And if the user was typing, there is probably a timer, lets clear that
clearTimeout(timer.current);
}
// Cleanup the timer if there was one and this effect is reloaded.
return () => clearTimeout(timer.current);
}, [query]);
As shown in the code below, I am using the useEffect hook to watch for changes to a percentage variable, then set a timer to increment that variable in 1 second. The callback will occur as soon as the page loads, kicking the process off.
The percentage variable is used to render a line chart that increments until a passed value is reached. (It basically shows a % pass rate, but on load the bars will move across the screen up to their %.)
Everything works fine, but the console flags the maximum update depth warning which points out it's trying to prevent an infinite loop when i'm using setState inside useEffect. I get the point here, but I know I'll never pass in a value above 100 and so there will never be an infinite loop. I also need the recursive functionality to have my charts appear to populate dynamically.
Even if I decided to ignore the warning, it's creating error in my testing if I mount the component.
Is there a way I can tell react that this won't be an infinite loop and get the error to go away? Or do I need a different approach?
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
let timer = null;
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer);
return;
}
timer = setTimeout(setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;
Thanks for any help
In timer = setTimeout(setPercent(newPercent), 1000);
setPercent(newPercent) will be called immediately. setTimeout requires a function (instead of a function call):
timer = setTimeout(() => setPercent(newPercent), 1000);
Another thing to keep in mind is that timer will be defined on each render, thus clearTimeout won't have any effect. To avoid this, you can create a ref so that the value will be remembered. Like so:
const timer = React.useRef()
using/setting the value is done to the current property
timer.current = ...
Working example:
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
const timer = React.useRef();
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer.current);
return;
}
timer.current = setTimeout(() => setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;