I'm currently running into some issues whilst developing a Typescript React App.
Underneath is my current code..
But it's not behaving like I would want it to behave. :)
So what I would like to achieve is that the data with getData(depth) runs whenever the component is being loaded and afterwards every 5 seconds.
But when the Depth changes with the Dropdown.item buttons, it should re-render and the getData() should be ran with the new depth value that we just set in the state.. and keep on rendering afterwards with the new value...
I've been struggling with this, so any help is very much appreciated!!
Thank you!
import React, { useState, useEffect } from "react";
const chart = () => {
const [depth, setDepth] = useState(20);
const [chartData, setChartData] = useState({})
//Getting the data when the app initially renders and should keep rendering every 5 seconds after that.
//When the value of the depth changes, we should stop getting the data with the old depth //value and should start a new interval of 5 seconds and just keep running with the new //depth value
//When first entering the app, this should run immediately with the initial depth state //(20)
useEffect(() => {
const interval = setInterval(() => {
//this code is not the actual code, just an example of what is running
const data = getData(depth)
//just fetched the new data, now setting it..
setChartData(data)
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<div>
<div>
<DropdownButton id="dropdown-basic-button" title="Depth Percentage">
<Dropdown.Item onClick={() => setDepth(5)}>5%</Dropdown.Item>
<Dropdown.Item onClick={() => setDepth(20)}>20%</Dropdown.Item>
</DropdownButton>
</div>
<div>
//Rendering the Chart here....
</div>
</div>
);
};
export default chart;
That's because useEffect hook take a second params called dependency array, where this dependency array is what matter for the inner callback(inisde useEffect) to access the latest values you want.
So your are not being totally truthful here, if the inner callback depends on depth to be in its latest update then you should include it in the dependency array
useEffect(() => { ... }, [ depth ]);
that's for the depth but writing this code will immediately cause problems because for each new depth value the inner callback will be called and the setInterval will re-run again (causing many many...many of intervals).
To solve this you should avoid using setInterval alll together in hooks based code.
If having interval is really important I have a suggestion for you
const [intervalCount, setIntervalCount] = useState(0);
const [depth, setDepth] = useState(20);
const [chartData, setChartData] = useState({})
useEffect(() => {
// when depth change re-fetch data and set it
const data: any = getData(depth);
setChartData(data);
}, [depth])
// simulate set interval behavior
// each 5 s this function will be re-invoked
useEffect(() => {
// re-fetch data and set it
const data: any = getData(depth);
setChartData(data);
// wait 5 s before cause a re-render
setTimeout(() => {
setIntervalCount(count => count + 1);
}, 5000);
}, [intervalCount]);
Updated: After rading from Dan Abramov blog
you can find a better elegant solution that use setInterval and hooks
Making setInterval Declarative with React Hooks
He made a custom hook called useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Usage be like
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
Related
I am doing a small React project.
I heard somewhere that using setState(()=>) is better than setState().
For instance, let [count, setCount] = useState(0)
setCount(prev=>prev+1) is better than setCount(count+1)
so I kept using the one having arrow function.
But, here in the code, I found that the arrow function one did not work, but the other one worked.
const addCard = () => {
setCards([
...cards,
{
topic: topicRef.current.value,
desc: descRef.current.value,
id: nanoid(),
done: false,
},
]);
topicRef.current.value = "";
descRef.current.value = "";
};
this code works fine, however if I start doing like setCards(prevState=>[...prevState, {}]), the first render works OK, but after that, the Ref value does not read the value I put properly. (I used useRef() for topic and desc.)
What is the difference between setState(()=>) and setState()?
The more classic example is when you use setInterval inside the useEffect. take a look at the following code:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
let interval = setInterval(() => {
// setCount((c) => c + 1);
setCount(count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
In this case, it renders 0 and then increments the count once after one second. The count value will remain at 1 after that.
The reason behind this is that count here represents your React state. This is a variable that holds a value. A closure is created over the outer scope where count is declared - which is the App component itself. Such variables can change value between function calls, but this is where React's immutability comes into play.
You can update the state using the second method as follows:
useEffect(() => {
let interval = setInterval(() => {
setCount((c) => c + 1); // this will update count value every second
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
There will always be a fresh state for that variable when using this method.
That's why you shouldn't jump into React without learning core JS first. React is not JavaScript, but rather a declarative JavaScript library that simplifies some things.
Codesandbox link of the above example: https://codesandbox.io/s/react-interval-gi4xl7
Great article by Dan referencing the same topic: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
My component fetches and displays posts using infinite scrolling (via an IntersectionObserver). The API call that the component makes is dependent on the current number of fetched posts. This is passed as an offset to the API call.
The desired effect is that, if the posts array is empty, its length is 0 and my API will return the first 5 posts. When the last post in the UI intersects with the viewport, another fetch is made, but this time the API call is passed an offset of 5, so the API skips the first 5 posts in the collection and returns the next 5 instead.
Here is my code:
export default function Feed() {
const [posts, setPosts] = useState([]);
const fetchPosts = async () => {
const newPosts = await postAPI.getFeed(posts.length);
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
};
useEffect(fetchPosts, []);
const observerRef = useRef(
new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
observerRef.current.unobserve(entry.target);
fetchPosts();
}
})
);
const observePost = useCallback((node) => node && observerRef.current.observe(node), []);
return (
<>
{posts.map((post) => {
const key = post._id;
const isLastPost = key === posts.at(-1)._id;
const callbackRef = !isLastPost ? undefined : observePost; //only if last post in state, watch it!
return <PostCard {...{ key, post, callbackRef }} />;
})}
</>
);
}
However, every time the fetchPosts function is called, the value it uses for posts.length is 0 - the number the function was originally created with.
Can someone explain to me why the closure over posts.length is stale here? I thought that every time a component re-renders, all nested functions within it were recreated from scratch? As such, surely the fetchPosts function should be using the latest value of posts.length every time it is called? Any help is appreciated! :)
You are creating a new IntersectionObserver on every render - not a good idea. However, only the first of all these created observers, the one you store in the ref (the ref that is never updated), is the one on which you call observe(), and that first observer uses a stale closure over the initial value of posts.
Instead, I would suggest creating the intersection observer(s) inside the observePost function:
const [posts, setPosts] = useState([]);
const fetchPosts = useCallback(async () => {
const newPosts = await postAPI.getFeed(posts.length);
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
}, [posts.length]);
// ^^^^^^^^^^^^
useEffect(fetchPosts, []);
const observePost = useCallback((node) => {
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
fetchPosts();
}
});
observer.observe(node);
}, [fetchPosts]);
// ^^^^^^^^^^
Important here is the dependency of the observePost callback on the fetchPosts callback which has a dependency on the length of posts, to avoid getting stale. It's probably also possible to solve this with refs, but I don't think they're necessary here.
What follows is my first attempt at the component, which is supposed to re-query data when the filters prop changes. Additionaly, whenever such a change is detected it should wrap back to the first page (first occurence of useEffect). Thirdly, the user should be able to go to the next page manually (nextPage callback).
function Fetcher(filters) {
const [page, setPage] = React.useState(0);
const nextPage = React.useCallback(() => {
setPage((p) => p + 1);
}, []);
React.useEffect(() => {
setPage(0);
}, [filters]);
React.useEffect(() => {
externalRequest(filters, page);
}, [filters, page]);
return <button onClick={nextPage}></button>;
}
Unfortunately this will naturally won't work properly, as resetting the page in the first effect hook will be performed asynchronously and picked up belatedly: if the user sits on page 1 and filters change, externalRequest will be fired twice in the following manner:
externalRequest(1, newFilters)
externalRequest(0, newFilters)
What I'd consider to be a workaround is either to store the page as a mutable reference as follows:
function Fetcher(filters) {
const page = React.useRef(0);
const nextPage = React.useCallback(() => {
page.current = page.current + 1;
}, []);
React.useEffect(() => {
page.current = 0;
}, [filters]);
React.useEffect(() => {
externalRequest(filters, page);
}, [filters, page.current]);
return <button onClick={nextPage}></button>;
}
or lift it up and pass from the outside as the prop (which is not ideal, as I want to avoid leaking this dependency outside). The issue with the former solution (using useRef) is that the component won't re-render when nextPage is called and the re-cycle would need to be forced. The problem with the latter is that it puts the 'burden' of managing the page dependency outside. It solves it though.
My question is whether there exists a way to structure the Fetcher so that we get the best of both worlds i.e. page state internally in the component and it remaining part of the state, rather than a reference? I'm sorry if this example seems a bit contrived, but I feel this pattern occurs quite often and I'd be really grateful for some input on the matter!
You can resolve this issue by removing the filters from the second effect dependency
Why? since you are making sure the page will trigger re-render for effect, so, by setting the filter in both effects, that means trigger twice, one for filter and then page also updates so this second render will be trigger.
You can check the demo here.
function Fetcher(props) {
const [page, setPage] = useState(0);
const nextPage = useCallback(() => {
setPage((p) => p + 1);
}, []);
useEffect(() => {
setPage(0);
}, [props.filters]);
useEffect(() => {
console.log(props.filters, page);
}, [page]);
return <button onClick={nextPage}>Next Page</button>;
}
There's an important note: each dependency will trigger re-render when its update, so that we put it inside the dependency array, but you need to make sure about the rendering tree too.
Also, you have another solution, by saving the old filter and checking the new value if it's equal old one or not.
Also, you can use the re-structure component and build function and call what you needed based on conditions...
And I don't recommend to useRef in this case... it's not a solution for a normal state, and I think the first solution is fair.
UPDATE 1: (for taking care about filters change):
function Fetcher(props) {
const [page, setPage] = useState(0);
const [oldFilters, setOldFilters] = useState(props.filters);
const nextPage = useCallback(() => {
setPage((p) => p + 1);
}, []);
useEffect(() => {
setPage(0);
if(JSON.stringify(props.filters) !== JSON.stringify(oldFilters)){
setOldFilters(props.filters);
}
}, [props.filters]);
useEffect(() => {
console.log(props.filters, page);
}, [page, oldFilters]);
return <button onClick={nextPage}>Next Page</button>;
}
Also, the setPage(() => () => 0); its a good option (like Pasato answer bellow) for handling this cae by update referance each time to trigger re-render.
Thank you Anees, that was so simple..
There's one edge caveat that's not addressed by your solution and it's the case in which filters change, while still on page 0 - in this case page dependent effect won't fire. I've ultimately opted in for the following:
function Fetcher(props) {
const [page, setPage] = useState(() => () => 0);
const nextPage = useCallback(() => {
setPage((p) => () => p() + 1);
}, []);
// do not run on the mount, but only when props change
// as otherwise the initial request is fired twice
useUpdate(() => {
setPage(() => () => 0);
}, [props.filters]);
useEffect(() => {
const actualPage = page();
}, [page]);
return <button onClick={nextPage}>Next Page</button>;
}
Laziness of the state is irrelevant in this case - I just "wrap it" in the function so that two page instances are referentially different, even if they return the same number. Also, page resetter doesn't run upon mount so that we don't query twice.
So I am using SwipeableViews no swipe between pages. My problem is each page have a different dynamic length. I have animateHeight as true but it only animate the height when the tab changes. In the documentation it says that the function updateHeight() can solve this.
https://react-swipeable-views.com/api/api/
Due to my lack of knowledge I just could not get the updateHeight since all the exampels I saw on class based app. I built my app in function based app. I just could not figure it out to pass it as props to call it later.
I've found a way to use the updateHeight function. It's a bit hacky, but it works 😅. I've added an interval and timeout to compensate for data loading in/slow machines. The useEffect cleans the interval when the page is unmounted.
There's probably a better solution, but this is what I've found so far!
export function SwipeView() {
const [ref, setRef] = useState(null);
const onRefChange = useCallback((node: SwipeableViews & HTMLDivElement) => {
// hacky solution to update the height after the first render. Height is not set correctly on initial render
setRef(node); // e.g. change ref state to trigger re-render
if (node === null) {
return;
} else {
interval = setInterval(() => {
// #ts-ignore typings are not correct in this package
node.updateHeight();
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 10000);
}
}, []);
useEffect(() => {
return () => clearInterval(interval);
}, [interval]);
return (
<SwipeableViews
animateHeight
ref={onRefChange}
>
{children}
</SwipeableViews>
);
}
We have migrated to 'React Functional Components' instead of 'Class based Component'. I cannot find the substitute logic for setState callback function. I.e, I have a functional component with state, and I want to create an event handler function that mutates the state multiple times in sequence, the caveat being that I dont know the current value of state (it may be true/false). The following example may make more sense.
const Example = () => {
const [ openDoor, setOpenDoor ] = useState(false);
// the following handler should swich 'openDoor' state to inverse of
// current state value. Then after setTimeout duration, inverse it again
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
// within setTimeout below, '!openDoor' does not work because it still
// receives the same value as above because of async nature of
// state updates
setTimeout(() => setOpenDoor(!openDoor), 500)
}
return(...);
}
In class based components, we had callback argument which would update state after previous update. How do I achieve the same in the above functional component using state hook?
I wonder if useEffect is the best solution. Specially when calling setTimeout within useEffect is going to cause an infinite loop since every time we call setOpenDoor, the app renders and then useEffect is called calling again a setTimeOut that will call a setOpenDoor function... Graphically:
setTimeout -> setOpenDoor -> useEffect -> setTimeout -> ... hell
Of course you could use an if statement wihin useEffect the same way that #ksav suggested but that does not accomplish one requirement of #Kayote:
I dont know the current value of state (it may be true/false)
Here is a solution that works without useEffect and accomplish the requirement stated above:
Code working in codesandbox
There, see the importance of this piece of code:
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(openDoor => !openDoor), 500);
};
Since we are using setTimeout, we need to pass callback to setOpenDoor instead of the updated state. This is because we want to send the 'current' state. If we sent the new state instead, by the time that setTimeOut processes that state, it will have changed (because we did it before setTimeOut executes its callback with setOpenDoor(!openDoor);) and no changes will be made.
You can use useEffect hook to see when the state change happend.
useEffect(() => {
// do something
console.log('openDoor change', openDoor)
}, [openDoor]);
I'll tell you that it works pretty much in the same way as this.setState, you just a pass a callback function which takes previous state as a parameter and returns new state(docs)
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
}
return(...);
}
In order for you know when it changes you can use useEffect callback, which's gonna be called each time something changes in the dependencies array(docs)
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
useEffect(() => {
console.log('openDoor changed!', openDoor)
}, [openDoor])
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
}
return(...);
}
:)
You can use useEffect hook to achieve this.
setOpenDoor(!openDoor);
useEffect(() => {
// Here your next setState function
}, [openDoor]);
For more information on hooks please check out https://reactjs.org/docs/hooks-effect.html
You should just using setTimeout within useEffect callback:
const App = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggle = () => setOpenDoor(prevOpen => !prevOpen);
useEffect(() => {
const id = setTimeout(() => toggle(), 1000);
return () => clearTimeout(id);
}, [openDoor]);
return <Container>isOpen: {String(openDoor)}</Container>;
};
import React, { useState, useEffect } from "react";
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
};
useEffect(() => {
console.log(openDoor);
if (openDoor) {
setTimeout(() => setOpenDoor(!openDoor), 1500);
}
}, [openDoor]);
return (
<>
<button onClick={toggleOpenDoor}>Toggle</button>
<p>{`openDoor: ${openDoor}`}</p>
</>
);
};
export default Example;
Codesandbox