React suspense prevent flashing of fallback spinner - javascript

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>
);
}

Related

React render component only for few seconds

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.

Why is a function passed through props into a child component behaving differently on a key press vs. button click?

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.

How to re-render a react component after returning something in a function?

I'm creating a react app with a method to draw some arrows among divs. The function doesn't produce arrows right away returns the JSX but just after I manually re-render the DOM by scrolling the page or do a route change. How can I force render the DOM after the return of the function?
drawArrows = () => {
const questionsList = this.state.questionsData;
return questionsList.map(each =>
<Arrow
key={each.order}
fromSelector={`#option${each.order}`}
fromSide={'right'}
toSelector={`#q${each.order}`}
toSide={'left'}
color={'#ff6b00'}
stroke={3}
/>
);
}
render (){
return(
...code
{this.drawArrows()}
)
}
you can do it easy with functional Component with useState and useEffect
import { useState } from 'react';
const ComponentName = () => {
const [arow, setArow] = useState();
const [questionsData, setQuestionsData] = useState([]);
const drawArrows = () => questionsData.map(each =>
<Arrow
key={each.order}
fromSelector={`#option${each.order}`}
fromSide={'right'}
toSelector={`#q${each.order}`}
toSide={'left'}
color={'#ff6b00'}
stroke={3}
/>
);
useEffect(() => {
setArow(drawArrows())
}, [])
return (
{ arow }
)
}

How to use setTimeout with a Loader

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)
};

FlatList performance warning issue when using the useEffect hook

I get a warning in my FlatList when using the useEffect hook to fetch data.
This is the complete component to reproduce the issue:
import React, { useState, useEffect } from "react";
import {
Text,
View,
FlatList,
ActivityIndicator,
SafeAreaView,
Button
} from "react-native";
const Test = props => {
const [people, setPeople] = useState([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
useEffect(() => {
setLoading(true);
fetch(`http://jsonplaceholder.typicode.com/photos?_start=${page}&_limit=20`)
.then(res => res.json())
.then(res => {
//console.log(res);
setPeople(people => [...people, ...res]);
setLoading(false);
});
}, [page]);
const loadMore = () => {
setPage(page + 20);
};
return (
<SafeAreaView style={{ flex: 1 }}>
<FlatList
data={people}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.id}</Text>
<Text>{item.title}</Text>
</View>
)}
ListFooterComponent={
loading ? (
<ActivityIndicator />
) : (
<Button title="Load More" onPress={loadMore} />
)
}
/>
</SafeAreaView>
);
};
export default Test;
This is the warning I'm getting
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
"contentLength": 4418,
"dt": 705,
"prevDt": 669,
}
It basically tells me to use PureComponent or shouldComponentUpdate, though, but AFAIK both do not work with either a functional component or the useEffect hook, do they?
Although I did not notice a (huge) performance drop, I'm still wondering if there's a workaround to fix this issue. Any help would be appreciated. Thank you very much.
Edit: Using a PureComponent does not fix the issue:
Created PureComponentTest.js
import React from "react";
import { Text, View } from "react-native";
const PureComponentTest = props => {
return (
<View>
<Text>{props.id}</Text>
<Text>{props.title}</Text>
</View>
);
};
export default PureComponentTest;
And in my Component I updated renderItem={renderItems}:
const renderItems = itemData => {
return (
<PureComponentTest
id={itemData.item.id}
title={itemData.item.title}
/>
);
};
I really don’t see anything wrong with your component. It’s a very simple pure component. It may simply because the fetched data is too big. Try reduce the number of pages fetched each time. say 5 or 10 pages
maybe the warning is for fetch into useEffect, so review the next documentation:
Some rules to keep in mind about the useEffect hook:
You cannot call a hook within a conditional; Hooks must be called in
the exact same order. Putting the useEffect inside of a conditional
could break that cycle; The function you pass the hook cannot be an
async function, because async functions implicitly return promises,
and a useEffect function either returns nothing or a cleanup function.
Consider use it:
fetch(`http://jsonplaceholder.typicode.com/photos?_start=${page}&_limit=20`)
.then(res => res.json())
.then(res => {
//console.log(res);
setPeople(people => [...people, ...res]);
})
.catch(error=> console.error(error))
.finally(() => setLoading(false));

Categories

Resources