I have the following logic that renders an image, the current flow is like the following: if the state is false a spinner shows then when the image loads the spinner disappears.
The core problem here is the state is re-rendering the component causing the image to load again I kind of ran out of options, on how to make an instant switch after the image loads.
It is not a loop but rather the image reloads again even if it is already loaded due to the render caused by setLoading(true).
How to prevent this reloading from happening. The useEffect logic is just a simulator for how it might take to load the image, but my real image coms from the icons variable.
export const iconsImg: React.FC<Props> = ({ img: string }) => {
useEffect(() => {
let newImg = new Image();
newImg.src = icons[img].props.src;
newImg.onload = () => {
setLoading(true);
};
}, []);
const icons: iconsInterface = {
a: <img className={classes.imgStyle} alt="a" src={link} />,
b: <img className={classes.imgStyle} alt="b" src={link} />,
}
const [loading, setLoading] = useState(false);
return (
<React.Fragment>
{!loading ? (
<Spinner>
) : (
icons[img]
)}
</React.Fragment>
)}
Issue
The problem with your last attempt is that you are starting with loading showing the Spinner, and you are trying to switch the state after the image is loaded, except that will never be the case, because the image is not mounted in the DOM.
Creating icons is not equivalent to creating img until it's added in the return and mounted into the DOM. And I think it's loading twice cause at some point you added the two of them in the DOM either directly or after loading changes.
Solution
You can simplify your component as below. Notice I removed icons and setting alt attribute (the one difference between the two) while creating the image. That setTimeout is so we see the loader. You can remove it later.
const IconsImg = ({ img }) => {
const [loading, setLoding] = React.useState(true);
return <div >{loading && "Loading..."} <img alt={img} style={{display: loading ? "none" : "block"}} onLoad= {()=> setTimeout(()=> setLoding(false), 1000)} src = "https://images.unsplash.com/photo-1657664042206-1a98fa4d153d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxlZGl0b3JpYWwtZmVlZHwxfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=500&q=60"/></div>;
};
/* The below code is to have a working example here at Stack Overflow */
ReactDOM.render(
<IconsImg img= "car" />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
newImg is a different object from icons[img]. When loading set
to true, browser loads same src this time for icons[img].
You can try this:
First set display:none for icons[img] if loading is false;
And:
<React.Fragment>
{loading || (
<Spinner>
)}
icons[img]
</React.Fragment>
Then:
useEffect(() => {
icons[img].onload = () => {
setLoading(true);
};
}, []);
or better:
<img className={classes.imgStyle} alt="a" src={link} onload={() => setLoading(true)}/>
Related
so i am creating a simple game and i render it if someone presses a start button on the first render. i want to preload all the images that the component is gonna use, so that when you hit the start button its already preloaded.
this is how the array of image paths look like:
const imagesArr = [
"/src/images/cheetah-min.webp",
"/src/images/dolphin-min.webp",
"/src/images/gorilla-min.webp",
"/src/images/lion-min.webp",
"/src/images/tiger-min.webp",
"/src/images/troll-min.webp",
"/src/images/werewolf.webp",
"/src/images/wolf-min.webp"
]
This is my App.jsx:
function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<div className="game-container">
{!isPlaying ? (
<div className="welcome-screen">
<h1 className="welcome-heading">Welcome to the game</h1>
<button
className="start-btn"
onClick={() => {
setTimeout(() => {
GAMESTARTS_SOUND.play();
setIsPlaying(true);
}, 3000);
}}
>
Play the game
</button>
</div>
) : (
<Game />
)}
</div>
);
}
as you can see there is this Game container that gets rendered if you press the play button. Inside that component im using the images. at the moment im just rendering them out using <img tags in the returned JSX from Game.jsx.
what I want is to preload the images before that component gets rendered? any ideas?
I have a recursive component that takes a couple seconds to render because of its complexity. I inspected the DOM and it turned out the component that contains that heavy component doesn't get inserted until after it fully loads. Before that happens, the page is just blank.
const SomeElement = (props) => {
// ...
return (
<div> // not inserted into DOM until HeavyComponent fully loads
// expected behavior: First the Loading... label is displayed, then the contents of the HeavyComponent
// actual behavior: not rendered until HeavyComponent renders, thus Loading... label and the component shows up at the same time
<div>Loading...</div>
<HeavyComponent />
</div>
);
};
I would like to display Loading... message when the other component loads in the background, like:
const [ heavyComponent, setHeavyComponent ] = useState(null);
React.asyncCreateElement(HeavyComponent, props)
.then((loadedComponent) => {
setHeavyComponent(loadedComponent)
})
if (!heavyComponent) {
return <div>Loading...</div>
}
return <div> { heavyComponent } </div>;
The closest I could find is React.lazy and Suspense, but it doesn't really match my usecase - I want that HeavyComponent to always be always visible. Using code-splitting didn't change the behavior.
So to reiterate: Is there a way to render a heavy (not because of async, but because of its complexity) component in the background (like in a service worker)?
Could you not just do
const SomeElement = (props) => {
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setFirstRender(false);
}, []);
return (
<div>
{ firstRender && <div>Loading...</div> }
{ !firstRender && <HeavyComponent /> }
</div>
);
};
so that you get the render you want, and then once it has finished generating the heavy component will replace it?
Basically I was trying to render a really really long list (potentially async) in React and I only want to render the visible entriesĀ±10 up and down.
I decided to get the height of the component that's holding the list, then calculate the overall list height/row height, as well as the scroll position to decide where the user have scrolled.
In the case below, SubWindow is a general component that could hold a list, or a picture, etc... Therefore, I decided it wasn't the best place for the calculations. Instead, I moved the calc to a different component and tried to use a ref instead
const BananaWindow = (props) => {
const contentRef = useRef(null)
const [contentRefHeight, setContentRefHeight] = useState(0)
useEffect(()=>setContentRefHeight(contentRef.current.offsetHeight), [contentRef])
//calc which entries to include
startIdx = ...
endIdx = ...
......
return (
<SubWindow
ref={contentRef}
title="all bananas"
content={
<AllBananas
data={props.data}
startIdx={startIdx}
endIdx={endIdx}
/>
}
/>
)
}
//this is a more general component. accepts a title and a content
const SubWindow = forwardRef((props, contentRef) => {
return (
<div className="listContainer">
<div className="title">
{props.title}
</div>
<div className="list" ref={contentRef}>
{props.content}
</div>
</div>
})
//content for all the bananas
const AllBanana = (props) => {
const [data, setData] = useState(null)
//data could be from props.data, but also could be a get request
if (props.data === null){
//DATA FETCHING
setData(fetch(props.addr).then()...)
}
return(
<Suspense fallback={<div>loading...</div>}>
//content
</Suspense>
}
PROBLEM: In BananaWindow, the useEffect is triggered only for initial mounting and painting. So I only ended up getting the offsetWidth of the placeholder. The useEffect does nothing when the content of SubWindow finishes loading.
UPDATE: Tried to use callback ref and it still only showed the height of the placeholder. Trying resize observer. But really hope there's a simpler/out of the box way for this...
So I solved it using ResizeObserver. I modified the hook from this repo to fit my project.
I want to display a component according to a state variable in the context provider. When I load this page, the closed is shown (fullRender is false initially). Clicking the button to display it, I see the canvas in the dom. When I click the Remove button, I call removeFullRender on the context which sets it false again.
const launchRender = () => {
setFullRender(true);
}
const removeFullRender = () => {
setFullRender(false);
}
As soon as I do this, react crashes (= shows white page and throws error in react-dom.development.js) with NotFoundError: Node was not found instead of showing the closed div again.
The full render component:
const FullRender = (props) => {
const context = useContext(FabricContext);
const [canvas, setCanvas] = useState();
useEffect(() => {
if(!context.fullRender){
return
}
setCanvas(new fabric.Canvas("full-canvas", {
selection: false,
}));
console.log(context.fullRender);
}, [context.fullRender]);
return (
<>
{context.fullRender ?
<>
<canvas
style={props.style}
id="full-canvas"
>
</canvas>
<Button onClick={context.removeFullRender}>X</Button>
</>
: <div>closed</div>
}
</>
);
}
I got it working in this example:
https://codesandbox.io/s/dank-frog-9wcgc?file=/src/App.js
The use of react fragments around the canvas caused the error (unsure why)
Changing to another tag should resolve it:
<div>
<canvas
style={props.style}
id="full-canvas"
>
</canvas>
<Button onClick={context.removeFullRender}>X</Button>
</div>
I make a demo application using final-form , but I am facing a issue , when I toggle my button (toggle button) image request fired again and again.
Step to reproduce this
1 ) On/off the Toggle button.see network request (DISABLE CACHE SHOULD BE CHECKED ) .
https://codesandbox.io/s/snowy-browser-sfpnz
const Abc = React.memo(() => {
return (
<ImageContainer>
<Image id="titleLogo" src={src} />
<TitleText>{title}</TitleText>
</ImageContainer>
);
});
const ShowImage = useCallback(() => {
return <Abc />;
}, []);
Take the styled-component calls out of 'AppWithIconToggle' function body. Give the title and src as props to Abc component and memo it, so it won't rerender when they are unchanged.
const Abc = React.memo(({ title, src }) => {
console.log("render", title, src);
return (
<ImageContainer>
<Image id="titleLogo" src={src} />
<TitleText>{title}</TitleText>
</ImageContainer>
);
});
https://codesandbox.io/s/hopeful-snowflake-h13uc