Load html section first before expensive section - reactjs

I have a time consuming loop for a table that is outputting around 300 cells with some custom computation on each, it takes 4-5 seconds to load which makes sense. So I want to display a loading modal whilst its doing this. However the modal only displays after the expensive loop finishes?
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if(loading){
console.log(loading, "Loading has started")
}
}, [loading])
return(
<>
<Modal show={loading}>Loading....</Modal>
<a onClick={() => setLoading(true)}>Load more</a>
// Expensive loop
array.map(() => {
// HMTL
<div>
----
</div>
})
</>
)
I can see from console log that state for "loading" has been updated but the modal has yet to display, so Im assuming its waiting for the loop to finish (which I dont want). Is there way to get it to output the html for the modal first or am I missing a fundamental of React?

If it's a slow computation you probably don't want to be doing it on each render, since components can render quite a few times.
Ideally you only want to render do those expensive calculations when the input data changes, which can be achieved by splitting the processing and rendering.
I'm not sure I completely understand how the loading part of this is working, but something like this example should help:
const [loading, setLoading] = useState<boolean>(false);
const [computedArray, setComputedArray] = useState([]);
useEffect(() => {
setComputedArray(array.map(//expensive loop here))
setLoading(false)
}, [array])
useEffect(() => {
if(loading){
console.log(loading, "Loading has started")
}
}, [loading])
return(
<>
<Modal show={loading}>Loading....</Modal>
<a onClick={() => setLoading(true)}>Load more</a>
// Cheap loop, turn data into markup
computedArray.map(() => {
// HTML
<div>
----
</div>
})
</>
)

I don't know what Modal is from so answering from a general perspective:
Personally, I'd keep the array in state and update it in a separate function, and use the && symbol to display the Loading element. This way you're keeping all of your logic regarding the expensive calculation and loading/unloading in one place.
export default function App() {
const [loading, setLoading] = useState(false);
const [array, setArray] = useState([]);
const getExpensiveCalculation = async () => {
setLoading(true);
const resultOfExpensiveCalculation = await ... // something here
// assuming result is a spreadable array too
setArray([...array, ...resultOfExpensiveCalculation]);
setLoading(false);
};
return (
<>
{loading && <Modal>Loading....</Modal>}
<a onClick={getExpensiveCalculation}>Load more</a>
{array.map(element => (
<div>
---
</div>
))}
</>
);
}

Related

React, while routing, url changes but the ui updates only after page refresh

I'm trying to build a project that uses infinite scrolling to display the user information in the ui taken from an external API. I've managed to fetch the data successfully from the API and also apply the infinite scrolling methodology after a lot of googling.
Now, I'm trying to make a dynamical routing work, so basically, when the user clicks on the card containing the user information that I'm getting from the API, It should be able to redirect to its personal page where there's gonna be even more personal data. So, I'm using routing for that and I managed to do that as well.
In the User.jsx file, I got the personal information about one single user that I get from the API. Also, on this page, I also have a list of this single user's friends and when I click on those friend's card, I should be able to redirect to their own personal page. I've managed to do that in a way, the functionality works, but nothing changes in the ui unless if I refresh the page after clicking the friend's card, after that, page displays the user and their personal data correctly. So that's essentially what I'm trying to fix, when I click on the card of user's friend list, the url changes immediately from localhost:3000/users/1 to localhost:3000/users/2, but the ui changes only after I refresh the page.
I'd appreciate any tips and help I can get, thank you in advance, here's the
User.jsx
const params = useParams();
const [users, setUsers] = useState();
const [items, setItems] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [page, setPage] = useState(1);
// Getting the single user
useEffect(()=>{
async function fetchData(){
const res = await axios(`http://sweeftdigital-intern.eu-central-1.elasticbeanstalk.com/user/${params.id}`);
console.log(res.data);
setUsers(res.data);
}
fetchData()
},[]);
// Getting the list of friends
const fetchData = (setItems, items) => {
let url = `http://sweeftdigital-intern.eu-central-1.elasticbeanstalk.com/user/${params.id}/friends/1/20`;
axios.get(url).then((res) => {
setItems([...items, ...res.data.list]);
console.log(res);
});
};
// Browsing more friends by changing the page from API
const moreData = () => {
let url = `http://sweeftdigital-intern.eu-central-1.elasticbeanstalk.com/user/${params.id}/friends/${page}/20`;
axios.get(url).then(res => {
setItems([...items, ...res.data.list]);
setPage(page+1)
setIsFetching(false)
});
}
// infinite scrolling methodology
const isScrolling =()=>{
if(window.innerHeight + document.documentElement.scrollTop!==document.documentElement.offsetHeight){
return;
}
setIsFetching(true)
}
useEffect(()=>{
fetchData(setItems,items);
window.addEventListener("scroll", isScrolling);
return () => window.removeEventListener("scroll", isScrolling);
},[]);
useEffect(() => {
if (isFetching){
moreData();
}
}, [isFetching]);
if(!users) return <p>Fetching...</p>
return (
<div>
<div className='container'>
<img src={users.imageUrl}/>
<h1 className='info'>Info</h1>
<div className='card-div'>
<div className='name-div'>
<h3 style={{fontWeight:"bold", fontSize:"30px", color:"#FF416C"}}>{users.prefix} {users.name} {users.lastName}</h3>
<h3 style={{fontStyle:"italic", fontSize:"22px"}}>{users.title}</h3>
</div>
<div className='details-div'>
<h3>Email: {users.email}</h3>
<h3>Ip Address: {users.ip}</h3>
<h3>Job Area: {users.jobArea}</h3>
<h3>Job Type: {users.jobType}</h3>
</div>
</div>
<h1 className='adress'>Address</h1>
<div className='address-div'>
<h3 style={{fontSize:"25px"}}>{users.company.name} {users.company.suffix}</h3>
<div>
<h3>City: {users.address.city}</h3>
<h3>Country: {users.address.country}</h3>
<h3>State: {users.address.state}</h3>
<h3>Street Address: {users.address.streetAddress}</h3>
<h3>ZIP: {users.address.zipCode}</h3>
</div>
</div>
</div>
<div className='friends-div'><h1 className='friends-h1'>Friends</h1></div>
<div className="card-container">
{items.map((user,key) => (
<Card key={key} id={user.id} prefix={user.prefix} name={user.name} lastName={user.lastName} image={user.imageUrl} job={user.title}/>
))}
</div>
</div>
)
You need to add your params.id to the useEffect's dependency list in order to know when it should try to update the UI
useEffect(()=>{
async function fetchData(){
const res = await axios(`http://sweeftdigital-intern.eu-central-1.elasticbeanstalk.com/user/${params.id}`);
console.log(res.data);
setUsers(res.data);
}
fetchData()
},[params.id]);

Incorrect use of useEffect() when filtering an array

I have this React app that's is getting data from a file showing in cards. I have an input to filter the cards to show. The problem I have is that after I filter once, then it doesn't go back to all the cards. I guess that I'm using useEffect wrong. How can I fix this?
import { data } from './data';
const SearchBox = ({ onSearchChange }) => {
return (
<div>
<input
type='search'
placeholder='search'
onChange={(e) => {
onSearchChange(e.target.value);
}}
/>
</div>
);
};
function App() {
const [cards, setCards] = useState(data);
const [searchField, setSearchField] = useState('');
useEffect(() => {
const filteredCards = cards.filter((card) => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
});
setCards(filteredCards);
}, [searchField]);
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={cards} />
</div>
);
}
you should Include both of your state "Card", "searchedField" as dependincies to useEffect method.once any change happens of anyone of them, your component will re-render to keep your data up to date,
useEffect(() => { // your code }, [searchField, cards]);
cards original state will be forever lost unless you filter over original data like const filteredCards = data.filter().
though, in a real project it's not interesting to modify your cards state based on your filter. instead you can remove useEffect and create a filter function wrapped at useCallback:
const filteredCards = useCallback(() => cards.filter(card => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
}), [JSON.stringify(cards), searchField])
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={filteredCards()} />
</div>
);
working example
about array as dependency (cards)
adding an object, or array as dependency at useEffect may crash your app (it will throw Maximum update depth exceeded). it will rerun useEffect forever since its object reference will change everytime. one approach to avoid that is to pass your dependency stringified [JSON.stringify(cards)]

The old "useEffect has a missing dependency" when calling an outside function

Yes, I know this question have been asked zillion times, but none of the answers are fit to my code.
My useEffect() calls an outside function (showIncrement()) that logs my increment state value. The problem is showIncrement() is also used by a button, so I can't move it inside the useEffect() scope.
I know a few solutions to this:
re-create the function inside useEffect(), but then I have two identical functions
use the React useCallback() function, React documentation call it the last resort, and other answer in another question also don't recommend using it, so I'm not really sure
The question is, what is the best way to solve this problem? Is it safe to use useCallback()?
Here's my code:
const App = () => {
const [increment, setIncrement] = React.useState(2);
const showIncrement = React.useCallback(() => console.log(increment), [
increment,
]);
React.useEffect(() => {
showIncrement();
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};
If showIncrement doesn't need access to update the state variables, the easiest way to fix this is to move it outside the component, so it won't be recreated on every render and have a stable reference:
const showIncrement = (increment) => console.log(increment);
const App = () => {
const [increment, setIncrement] = React.useState(2);
React.useEffect(() => {
showIncrement(increment);
}, [increment]);
return (
<div className="App">
<button type="button" onClick={() => showIncrement(increment)}>
Show Increment
</button>
</div>
);
};
Another way is to use the useCallback hook:
const App = () => {
const [increment, setIncrement] = React.useState(2);
const showIncrement = React.useCallback(() => console.log(increment), [
increment
]);
React.useEffect(() => {
showIncrement();
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};
As I understand this situation, you want to:
Re-use showIncrement logic.
Run showIncrement on component's mount.
Not violate linter warnings.
The problem here is that showIncrement depends on state value increment, so either way, your useEffect has to include increment in dep array.
Usually, you go for useCallback when its dep array ([increment] in this case) not frequently changes.
So depending on your use case, since showIncrement triggered only onClick it seems like a good choice.
const App = () => {
const [increment] = React.useState(2);
const isMounted = useRef(false);
const showIncrement = useCallback(() => {
console.log(increment);
}, [increment]);
React.useEffect(() => {
if (!isMounted.current) {
showIncrement();
}
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};

React Component is rendering twice

I have no idea why, the first render shows an empty object and the second shows my data:
function RecipeList(props) {
return (
<div>
{console.log(props.recipes)}
{/*{props.recipes.hits.map(r => (*/}
{/* <Recipe initial="lb" title={r.recipe.label} date={'1 Hour Ago'}/>*/}
</div>
)
}
const RECIPES_URL = 'http://cors-anywhere.herokuapp.com/http://test-es.edamam.com/search?i?app_id=426&q=chicken&to=10'
export default function App() {
const classes = useStyles();
const [data, setData] = useState({});
useEffect(() => {
axios.get(RECIPES_URL)
.then(res => {
setData(res.data);
})
.catch(err => {
console.log(err)
})
}, []);
return (
<div className={classes.root}>
<NavBar/>
<RecipeList recipes={data}/>
<Footer/>
</div>
);
}
I don't know why and I have struggled here for over an hour (React newbie), so I must be missing something.
This is the expected behavior. The reason you see two console logs is because, the first time RecipeList is called with no data (empty object), and the second time when the data becomes available. If you would like to render it only when the data is available you could do something like {Object.keys(data).length > 0 && <RecipeList recipes={data}/>}. By the way this is called conditional rendering.
This is perfectly normal, React will render your component first with no data. Then when your axios.get returns and update data, it will be rendered again with the new data

I am using React Hook correctly?

I want to apply React hook for infinite scroll. So, I end up with this code:
export default function index() {
const [allTracks, setAllTracks] = useState([]);
const [offset, setOffset] = useState("");
const { tracks, error, loading, lastVisible } = useFetchPublicTracks(
myApiEndPoint.TRENDING_TRACKS,
5,
offset
);
//concat tracks when got data successfully
useEffect(() => {
if (!loading && tracks) setAllTracks([...allTracks, ...tracks]);
}, [loading, tracks]);
console.log("tracks", allTracks);
console.log("lastVisible", lastVisible);
console.log("loading", loading);
return (
<div>
<button className="" onClick={() => setOffset(lastVisible)}>
Load More
</button>
<Main></Main>
</div>
);
}
When I click "Load More" button, new offset will be setted. After that, component re-render again and call Api with new offset.
I want ask that I am using React hook correctly ? There is any way better ? Also, Do I need to use useCallback/useMemo in my use case ?

Resources