Render heavy React component in the background - javascript

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?

Related

How do I render same component depending on the page the user is currently on?

I have a component called a Card. I render it twice on page 1 and page 2 and it displays somewhat different content depending on the page. Here is my initial approach.
const Card = () => {
const [cardState, setCardState] = useState("firstpage");
const firstPageProducts = productArray.slice(0, 4);
const secondPageProducts = productArray.slice(5, productArray.length);
const [shownImages, setshownImages] = useState([]);
useEffect(() => {
const shownImages = firstPageProducts.slice(0, 2);
setshownImages(shownImages);
}, []);
if (cardState === "firstpage") {
return (
<div>
some first page stuff
</div>
);
} else if (cardState === "secondpage") {
return (
<div>
some second page stuff
</div>
);
}
};
I want it to have a way for it to be stateless in a way that I can define from the page in which I'm rendering the card state. I also want it to persist in that if I go back to page 1 it loads correctly page 1 stuff.
right now if I pass down the card state as a prop to the app it sometimes throws an error claiming that the component has not been rendered at all or if it does it changes both the first-page component and the second-page component. I'm fairly new to react and I've heard of stateless components. I was thinking this has the potential to be one of them.
How can I achieve this?
I think what you want is prop. Prop is something passed to child component in your case Card from parent component like a Deck component or something. Idk if that exist in your app.
eg
const Deck = () => {
return (
<div>
<Card cardContent='I am first' productList={[1, 2, 3]} />
<Card cardContent='I am second' productList={[4, 5, 6]} />
</div>
);
};
const Card = ({ cardContent, productList }) => {
return <div>
<h1>{cardContent}</h1>
{productList.map((product) => {
return <p>{product}</p>
})}
</div>;
};

react - Add a component after main component has loaded

I have a functional react component like this:
function ComponentA(){
function AddComponentB(){
return <><ComponentB /></>
}
useEffect(() => {
AddComponentB();
}, []);
return
<>
<div id="parent"></div>
</>
}
Now I have understood that everything under useEffect is loaded once the ComponentA is loaded. I want to add the ComponentB to div with id parent. Please help me understand how do I specify where to add the component.
P.S. I know I can do it by document.getElementById("parent").append(ComponentB) but I am looking for other ways.
Try using conditional rendering, like below :
export default function ComponentA() {
const [renderComponentB, setRenderComponentB] = useState(false)
useEffect(() => {
setRenderComponentB(true);
}, []);
return(<div id="parent">
{renderComponentB && <ComponentB/>}
</div>)
}
No, you do not manipulate the DOM directly when using React.
You need to have a "flag" that dictates if you want to render the extra component or not.
Something like
function ComponentA(){
const [renderComponentB, setRenderComponentB] = useState(false);
useEffect(() => {
setRenderComponentB(true);
}, []);
return (
<>
<div id="parent">
{renderComponentB && <ComponentB/>}
</div>
</>
);
}
Although i am not sure why you want to delay the ComponentB for just one rendering cycle.
As far as the getBoundingClientRect of ComponentA, there is no such thing, as that depends on what the component actually renders in the DOM. ComponentA in it self is not part of the DOM.
In your specific case, though you could add a ref to the #parent element and use that for the getBoundingClientRect, since it is your "container" element of the ComponentA
function ComponentA(){
const [renderComponentB, setRenderComponentB] = useState(false);
const parentRef = useRef();
useEffect(() => {
setRenderComponentB(true);
}, []);
useLayoutEffect(() => {
const rect = parentRef.current.getBoundingClientRect();
// do what you want with the rect here
// if you want to apply values to the ComponentB
// add them to a state variable and use those when
// rendering the ComponentB
}, [])
return (
<>
<div id="parent" ref={parentRef}>
{renderComponentB && <ComponentB/>}
</div>
</>
);
}
You should call method that returns component in render.
You do not need useEffect as i understood from your answer.
function ComponentA(){
function AddComponentB(){
return <><ComponentB /></>
}
return
<>
<div id="parent">
{AddComponentB()}
</div>
</>
}

Get the height (dimensions) of a Suspense element after loading in react

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.

How to re-render a component with React-Router <Link> pointing to the same URL

To keep it simple, the detail page fetches data on mount based on the movie ID in the URL, this coming from path='movie/:id' in the Route.
It's child is called Recommended, which shows you recommended movies based again on the current URL.
class MovieDetailPage extends React.Component {
// Fetch movies and cast based on the ID in the url
componentDidMount() {
this.props.getMovieDetails(this.props.match.params.id)
this.props.getMovieCast(this.props.match.params.id)
}
render() {
<div>
Movies here
</div>
<Recommended id={this.props.match.params.id}/>
}
}
The Recommended component fetches data based on the current movie as well and generates another tag pointing to another movie.
class Recommended extends React.Component {
componentDidMount() {
this.props.getRecommended(this.props.id)
}
render() {
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{
this.props.recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
>
</img>
</Link>
)
})
}
</div>
</>
)
}
}
Now how can I trigger another render of the parent component when clicking the Link generated in the Recommended component? The URL is changing but this won't trigger a render like I intent to do.
UPDATE:
<Route
path="/movie/:id"
render={(props) => (
<MovieDetailPage key={props.match.params.id}
{...props}
)}
/>
I passed in a unique key this time that triggered the re-render of the page. I tried this before but I might've screwed up the syntax.
This post got me in the right direction: Force remount component when click on the same react router Link multiple times
Add a key to the page
If you change route but your page is not getting its "mount" data then you should add a key to the page. This will cause your page to rerender and mount with the new id and get the data again.
You can read more about react keys here
A key tells react that this is a particular component, this is why you see them in on lists. By changing the key on your page you tell react that this is a new instantiation of the component and has changed. This will cause a remount.
Class component example
class MyPage extends React.Component {
componentDidMound() {
// this will fire each time the key changes since it triggers a mount
}
render() {
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
}
Functional component example
const MyPage = (props) => {
React.useEffect(() => {
// this will fire each time the key changes
}, []);
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
You can add another React lifecycle method that triggers on receiving new props (UNSAFE_componentWillReceiveProps, componentDidUpdate, getDerivedStateFromProps) in your Recommended component like this:
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
nextProps.getRecommended(nextProps.id);
};
}
You can also add key to your component (which forces it to re-render completely if key changed) like this:
<Recommended key={this.props.match.params.id} id={this.props.match.params.id}/>
You can also use React Hooks to handle this more easily with useEffect:
const Recommended = (props) => {
const { id, getRecommended, recommended } = props;
useEffect(() => {
id && getRecommended(id);
}, [id]);
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
></img>
</Link>
);
})}
</div>
</>
);
};
Note: adding key to component and complete its re-render is not best practice and you should be using Component's lifecycles to avoid it if possible

Manipulating DOM in componentDidMount() without setTimeout

I want to manipulate the DOM in ReactJS in the componentDidMount() method. My problem is that at this time the DOM isn't fully rendered somehow and I need a setTimeout function, which I would rather omit.
When I console.log the scrollHeight of the rendered element in componentDidMount() it gives me a different number as when I wait for let's say 100 milliseconds.
What I want to achieve is to scroll down to the end of an element which is described here How to scroll to bottom in react?
The component is a modal-window which renders {this.props.children} of another component. The modal-window is rendered into the DOM with visibility: hidden and opacity: 0 and it has the height of the window, when it first appears on the page. By clicking on a button it shows up and still has the height of the window until I wait some milliseconds.
I guess, I do something wrong here when setTimeout is needed, but I didn't found out what.
I also tried to change the DOM in the componentDidUpdate() method with the same results.
I wrote this code in the modal-window component:
componentDidMount() {
console.log(document.querySelector('.myModal').scrollHeight);
setTimeout(function() {
console.log(document.querySelector('.myModal').scrollHeight);
}, 100);
}
First console.log gives me for example 497 and the second one something like 952.
Update
I have a modal-window component which renders a child like this for example for my inbox-thread:
<Modal>
<InboxThread />
</Modal>
The problem was, that I needed to wait until the modal-window component rendered its children like this in the Modal.js:
render() {
return (
<React.Fragment>
{this.props.children}
</React.Fragment>
);
}
So my solution in the end was to hand over a method in the props from the parent component where I call the modal to check if componentDidUpdate() in Modal.js.
My code looks now like this in the parent component:
...
export default class InboxThreadList extends React.Component {
constructor(props) {
super(props);
this.scrollToModalBottom = this.scrollToModalBottom.bind(this);
}
render() {
return (
<React.Fragment>
...
<Modal onRender={this.scrollToModalBottom}>
<InboxThread/>
</Modal>
</React.Fragment>
)
}
scrollToModalBottom() {
const myModalObject = document.querySelector('.myModal');
myModalObject.scrollTop = myModalObject.scrollHeight;
}
}
And in the Modal.js:
...
export default class Modal extends React.Component {
...
componentDidUpdate() {
if ('onRender' in this.props) {
this.props.onRender();
}
}
render() {
return (
<div className={'myModal'}>
{this.props.children}
</div>
);
}
I know! I still should work with refs instead of document.querySelector and I will do as described here React - Passing ref from dumb component(child) to smart component(parent).
If you use a ref - as long as the element is always rendered in render() - it is guaranteed to resolve before componentDidMount runs:
componentDidMount() {
// can use any refs here
}
componentDidUpdate() {
// can use any refs here
}
render() {
// as long as those refs were rendered!
return <div ref={/* ... */} />;
}
componentDidMount called BEFORE ref callback
So in your case, you might code it a bit like this:
componentDidMount() {
console.log(this.mymodal.scrollHeight)
}
render() {
return <div className="mymodal" ref={ref => this.mymodal = ref} />
}

Categories

Resources