I have a simple functional component where i click a button and it is shown, i am trying to get my imported spinner to show for 2 seconds when the button is clicked and then show my imported component after the two seconds, however i am only able to get the spinner to show 2 seconds after the button is clicked and cannot get it to stop
import React, { useState } from "react";
import Hello from "./Hello";
import Spinner from '../Spinner/Spinner'
import "./styles.css";
export default function App() {
const [show, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const helloHandeler = () => {
setTimeout(() => {
setLoading(!loading)
}, 2000)
setShow(!show);
};
if (loading) return <Spinner />
return (
<div className="App">
<h1>Adding a Spinner</h1>
<div className="bodyContainer">
{!show && <button onClick={helloHandeler}>Click me</button>}
{show && <Hello />}
</div>
</div>
);
}
Working example can be found here: https://codesandbox.io/s/gallant-engelbart-y3jus
You can add useEffect hook to update the DOM.
You are only updating the loading flag inside handler. React does not know that it needs to update the DOM.
useEffect(() => {
if (loading) {
setTimeout(() => {
setLoading(false);
}, 2000);
}
}, [loading]);
Forked codesandbox: https://codesandbox.io/s/inspiring-liskov-t53fv
When you trigger helloHandeler() it is registering the setTimeout() to start only after two seconds! This is the behaviour of setTimeout().
Instead, you should setLoading() imediatly, and then setTimeout to stop loading 2sec after. Maybe you would want to setShow() after the two sec also, so place it inside the setTimeout().
update
Also, remmember that JS works asynchronusly, so, when you register setTimeout, the loading is not true yet.
const helloHandeler = () => {
setLoading(true)
setTimeout(() => {
setLoading(false)
setShow(!show);
}, 2000)
};
Related
This part of my code shows a Skeleton which is a placeholder preview for CardMedia.
{loading ? (
<Skeleton
variant="rectangular"
width={308}
height={420}
animation="wave"
/>
) : (
<CardMedia
sx={{ height: 420 }}
image={item.itemMainPic}
title="green iguana"
/>
)}
I'm showing the Skeleton when the loading is true and using useEffect after 3 seconds I setLoading(false):
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 3000);
}, []);
const [loading, setLoading] = useState(true);
I have other CardMedias that I can show them using the pagination:
const handleChange = (event, value) => {
setCurrentpage(value);
};
<Pagination
count={Math.ceil(Myjson.length / postsPerpage)}
onChange={handleChange}
/>
But when I click on different pagination buttons, it no longer shows a Skeleton, so that means I have to reload the window, in other words the useEffect works only once and when I load other cards using pagination there is no Skeleton.
Is there a way to show Skeleton whenever I click on pagination buttons and then disable it after 3 seconds?
Since the code was too long, I simplified it.
If I understand the question correctly you are basically wanting to render the skeleton on the initial component mount and any time the currentPage state updates. For this I believe you can add currentPage as a dependency to the useEffect hook.
Example:
const [currentPage, setCurrentPage] = React.useState(....);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
const timer = setTimeout(() => {
setLoading(false);
}, 3000);
// Return cleanup function to clear timer if currentPage updates
// or the component unmounts prior to timeout expiring.
return () => {
clearTimeout(timer);
};
}, [currentPage]);
...
With the above logic there's a chance that the UI will update and momentarily render the next page because the currentPage state updates and the component rerenders just prior to the next useEffect call that triggers conditionally rendering the skeleton. If this is an issue and you want to avoid it then another solution would be to enqueue setting the loading state at the same time as enqueueing the currentPage state update. This is so they are updated at the same time during the same render.
Example:
const [currentPage, setCurrentPage] = React.useState(....);
const [loading, setLoading] = useState(true);
const timerRef = React.useRef();
const triggerSkeleton = () => {
setLoading(true);
// Clear any previously running timeout
clearTimeout(timerRef.current);
// Set new timeout
timerRef.current = setTimeout(() => {
setLoading(false);
}, 3000);
};
useEffect(() => {
// Trigger the skeleton and timer on initial component mount
triggerSkeleton();
// Return cleanup function to clear timer if
// the component unmounts prior to timeout expiring.
return () => {
clearTimeout(timerRef.current);
};
}, []);
const handleChange = (event, value) => {
setCurrentpage(value);
triggerSkeleton();
};
...
In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.
I have a React project where the parent component (functional) holds state to determine what formatting is applied to a list. When the user clicks a button, a modal is generated - I pass an anonymous function to that modal as props (onMainButtonClick), which when called flips the state and changes the formatting of the list (logic for this is in parent component).
When I use the button in my modal component (onClick={() => onMainButtonClick()}), the code works as expected. However, I would also like an enter press to trigger this. Therefore I have the following code implemented for this, but it doesn't function as expected. The modal closes and the function fires (I know this as I put a console log in there...) but the state that impacts the formatting is not changed.
EDIT: Having made a proposed change below (to memo'ize the onEnterPress function so it gets removed properly), here's the full code for the modal:
import { React, useRef, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
const Modal = ({
title,
description,
isOpen,
onClose,
onMainButtonClick,
mainButtonText,
secondaryButtonText,
closeOnly,
}) => {
const node = useRef();
const onEnterPress = useCallback(
(e) => {
if (e.key === "Enter") {
onMainButtonClick();
}
},
[onMainButtonClick]
);
useEffect(() => {
window.addEventListener("keydown", onEnterPress);
return () => {
window.removeEventListener("keydown", onEnterPress);
};
}, [onMainButtonClick]);
const handleClickOutside = (e) => {
if (node.current.contains(e.target)) {
return null;
}
onClose();
};
if (!isOpen) return null;
return ReactDOM.createPortal(
{// JSX here, removed for brevity},
document.body
);
};
export default Modal;
And the code for the parent (which shows the modal and passes down onMainButtonClick):
import { React, useState } from "react";
import { decode } from "html-entities";
import useList from "../../queries/useList";
import useBuyItem from "../../mutations/useBuyItem";
import Header from "../Shared/Header";
import Description from "../Shared/Description";
import ListItem from "./ListItem";
import BuyOverlay from "./BuyOverlay";
import Modal from "../Modal";
import ViewOperations from "./ViewOperations";
const View = (props) => {
const [buyOverlayOpen, setBuyOverlayOpen] = useState(false);
const [viewBuyersOverlayOpen, setBuyersOverlayOpen] = useState(false);
const [viewBuyers, setViewBuyers] = useState(false);
const [buyItemId, setBuyItemId] = useState();
const { isLoading, isError, data, error } = useList(props.match.params.id);
const buyItemMutation = useBuyItem(buyItemId, props.match.params.id, () =>
setBuyOverlayOpen(false)
);
if (isLoading) {
return <div>Loading the list...</div>;
}
if (isError) {
return <div>An error occured, please refresh and try again</div>;
}
return (
<div className="w-full">
<div className="mb-10 text-gray-600 font-light">
<Header text={data.name} />
<Description text={data.description} />
<ViewOperations
toggleViewBuyers={() => setViewBuyers(!viewBuyers)}
toggleBuyersOverlay={() =>
setBuyersOverlayOpen(!viewBuyersOverlayOpen)
}
viewBuyers={viewBuyers}
/>
<div id="list" className="container mt-10">
{data.items.length === 0
? "No items have been added to this list"
: ""}
<ul className="flex flex-col w-full text-white md:text-xl">
{data.items.map((item) => {
return (
<ListItem
item={decode(item.item)}
description={decode(item.description)}
itemId={item._id}
isBought={decode(item.bought)}
boughtBy={decode(item.boughtBy)}
boughtDate={decode(item.boughtDate)}
viewlink={item.link}
handleBuy={() => {
setBuyItemId(item._id);
setBuyOverlayOpen(true);
}}
key={item._id}
viewBuyers={viewBuyers}
/>
);
})}
</ul>
</div>
</div>
<BuyOverlay
isOpen={buyOverlayOpen}
message="Message"
onClose={() => setBuyOverlayOpen(false)}
onConfirmClick={(buyerName) => {
buyItemMutation.mutate(buyerName);
}}
/>
<Modal
title="Title"
description="Description"
isOpen={viewBuyersOverlayOpen}
onClose={() => {
setBuyersOverlayOpen(false);
}}
onMainButtonClick={() => {
setViewBuyers(true);
setBuyersOverlayOpen(false);
}}
mainButtonText="OK"
secondaryButtonText="Cancel"
/>
</div>
);
};
export default View;
Any ideas for why this is happening? I have a feeling it might be something to do with useEffect here, but I'm a bit lost otherwise...
onEnterPress is most probably directly defined in your functional component and that's why its reference changes every time your component re-renders. Your useEffect closes over this newly defined function on each render so your handler is not going to work as you expected.
You can wrap your onEnterPress with useCallback to memoize your handler onEnterPress so its definition stays the same throughout each render just like this:
const onEnterPress = useCallback(e => {
if (e.key === "Enter") {
onMainButtonClick();
}
}, [onMainButtonClick]); // Another function dep. You can also wrap it with useCallback or carry over its logic into the callback here
useEffect(() => {
window.addEventListener('keydown', onEnterPress);
return () => {
window.removeEventListener('keydown', onEnterPress);
};
}, [onEnterPress]);
I figured this out in the end - turns out it was as simple as my enterPress callback needing a preventDefault statement before the if block (e.preventDefault()).
There must have been something else on the page that was capturing the enter press and causing it to behave strangely.
I am having an issue with my app in that it re renders a new joke twice when I click the new button function. Here is my code:
import React, { useState, useEffect } from "react";
import { Typography, Button } from "#material-ui/core";
import Navigation from "../Navigation";
export default function RandomJoke() {
const [isLoaded, setLoaded] = useState(false);
const [jokeData, setJokeData] = useState({});
const [loadNewJoke, setLoadNewJoke] = useState(false);
function useFetch() {
async function fetchMyAPI() {
let response = await fetch("https://icanhazdadjoke.com/slack");
response = await response.json();
setJokeData(response);
setLoaded(true);
}
useEffect(() => {
fetchMyAPI();
if (loadNewJoke) setLoadNewJoke(false);
}, [loadNewJoke]);
}
useFetch();
function reloadJoke() {
setLoaded(false);
setLoadNewJoke(true);
}
return (
<>
<Navigation mainpage="RandomJoke" />
<Typography variant="h6">Random Dad Joke</Typography>
{isLoaded && <div>{jokeData.attachments[0].text}</div>}
{!isLoaded && <div>loading...</div>}
{isLoaded && (
<Button variant="contained" onClick={() => reloadJoke()}>
New one
</Button>
)}
</>
);
}
I tried adding a newjoke state hook but still couldn't work it out. Thank you
That useEffect fires whenever the value of loadNewJoke changes, right? Not just when loadNewJoke is set to true. Look closely at the calls made after a button press, and how many times setLoadNewJoke is called.
Try to move:
if (loadNewJoke) setLoadNewJoke(false);
In your fetchMyApi function. I'm guessing when you hit the button, you trigger the effect cuz u change you deps value in this case to true. Then before effect is over you change it again to false which will triggeer re-run on your effect.
But why you dont just trigger fetchApi in your callback on button click, this way you can remove 1 state [loadNewJoke, setLoadNewJoke], will also remove the useEffect and make code cleaner over all
You're using useEffect wrong, i suggest u take a look at the Rules of Hooks
Don’t call Hooks inside loops, conditions, or nested functions.
I followed what Andom Miltev said about triggering the async function directly in my callback and it now works smoothly - thank you everyone for the help :)
import React, { useState, useEffect } from "react";
import { Typography, Button } from "#material-ui/core";
import Navigation from "../Navigation";
export default function RandomJoke() {
const [isLoaded, setLoaded] = useState(false);
const [jokeData, setJokeData] = useState({});
async function fetchMyAPI() {
setLoaded(false);
let response = await fetch("https://icanhazdadjoke.com/slack");
response = await response.json();
setJokeData(response);
setLoaded(true);
console.log("fired 1");
}
useEffect(() => {
fetchMyAPI();
}, []);
return (
<>
<Navigation mainpage="RandomJoke" />
<Typography variant="h6">Random Dad Joke</Typography>
{isLoaded && <div>{jokeData.attachments[0].text}</div>}
{!isLoaded && <div>loading...</div>}
{isLoaded && (
<Button variant="contained" onClick={() => fetchMyAPI()}>
New one
</Button>
)}
</>
);
}
I am wondering if there is a good way to prevent the flashing of the fallback in react. I am using react router and the issue is that when a component gets suspended the fallback loader flashes really quick and it is pretty annoying. I saw the answer here React suspense/lazy delay? which would look like the following:
const Home = lazy(() => {
return Promise.all([
import('./components/Home'),
new Promise(resolve => setTimeout(resolve, 500))
]).then(([moduleExports]) => moduleExports);
});
but my issue with this is that I have an overlay loading spinner with a transparent background and the component doesn't actually load until the promises are resolved. This leaves the page hanging without content for a half of a second and is actually more annoying then the flashing of the spinner.
So I guess the question is has anyone found a good way to deal with this issue. I would really like to add something like nprogress to the page but can't figure out how I would implement this with React.suspense. I may just have to go back to using react loadable but I really don't want to when react comes with basically the same functionality out of the box.
I recently was facing the same issue and I ended up with this implementation of a component that I use for the fallback of the suspense.
The idea is simple, just don't show anything for a certain amount of time, then show the loader. So let's say for like 300ms there won't be anything displayed, after the delay it should display (in this case) the ContentLoader or anything you like.
In Typescript as lazy-loader.ts
import { FC, useEffect, useState } from 'react';
import ContentLoader from './content-loader'; // or any spinner component
export interface LazyLoaderProps {
delay?: number;
}
const LazyLoader: FC<LazyLoaderProps> = ({
delay = 250,
...props
}) => {
const [show, setShow] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setShow(true);
}, delay);
return () => {
clearTimeout(timeout);
};
}, [delay]);
return show ? <ContentLoader {...props} /> : null;
};
export { LazyLoader as default };
then use like so
import LazyLoader from "./lazy-loader"
// ...
<Suspense fallback={<LazyLoader delay={300} />}>...</Suspense>
That does not delay the import. (which I also thought was not optimal)
Let me know if this helps.
import React, { Suspense, lazy } from "react";
const Home = lazy(() => {
return Promise.all([
import("./home"),
new Promise(resolve => setTimeout(resolve, 300))
]).then(([moduleExports]) => moduleExports);
});
function FullSpinner() {
return (
{/** full spinner jsx goes here */}
<div className="full-spinner">
<p>loading....</p>
</div>
)
}
function App() {
return (
<div className="App">
<h1>app component</h1>
<Suspense fallback={<FullSpinner />}>
<Home />
</Suspense>
</div>
);
}
Edit:
import React, { Suspense, lazy, useState, useEffect } from "react";
const Home = lazy(() => {
return Promise.all([
import("./home"),
new Promise(resolve => setTimeout(resolve, 500))
]).then(([moduleExports]) => moduleExports);
});
function FullSpinner() {
return (
<div className="full-spinner">
<p>loading....</p>
</div>
);
}
const LazyLoading = ({ delay, loader: Loader, children }) => {
const [ready, setReady] = useState(false);
useEffect(() => {
setTimeout(() => setReady(true), delay);
}, [delay]);
return ready ? (
<Suspense fallback={<Loader />}>{children}</Suspense>
) : (
<Loader />
);
};
function App() {
return (
<div className="App">
<h1>app component</h1>
<LazyLoading delay={2000} loader={FullSpinner}>
<Home />
</LazyLoading>
</div>
);
}