Avoiding infinite re-rendering in react component - javascript

I have a Supabase database called threads with 5 entries, I am trying to make a new React component for every column in the database but I am running into some issues.
The issue that it's giving me is that it's stopping the program to prevent an infinite loop, I'd assume this is because in my code I am updating my useState every time I render my Component which I am aware of, but I'm not sure how I would get around this problem.
Component.js:
import {useState} from "react";
import {supabase} from "../../utils/supabaseClient";
export default function Sidebar() {
const [newThreads, setNewThreads] = useState([]);
// this is where I am kinda stuck
const threadList = [];
(async () => {
let {data: threads, error} = await supabase
.from('threads')
.select('thread_title');
threadList.push(threads); // how do I get the current index of the loop (and limit it to only get 5 entries?)
threadList.forEach(function (value) {
console.log(value)
})
})();
setNewThreads(threadList);
return (
<div className="sidebar">
<div className="sidebar-widget">
<span className="widget-title">New Threads</span>
</div>
<div className="sidebar-widget">
<span className="widget-title">New Members</span>
{(Object.entries(newThreads) || []).map(([key, value]) => {
return (
<div className="widget-cell">
<div className={"widget-cell-image"}/>
<div className="widget-cell-content">
<span className={"widget-cell-title"}>{value}</span>
<div className="widget-cell-values">
<span className={"widget-cell-by-user"}>by harley_swift,</span>
<span className={"widget-cell-time"}>22:02</span>
</div>
</div>
</div>
);
})}
</div>
</div>
)
}
Any help with an explanation would be greatly appreciated!

The way to get around the problem of infinite re-rendering is to use useEffect to fetch your data:
useEffect(() => {
// this is where I am kinda stuck
const threadList = [];
(async () => {
let {
data: threads,
error
} = await supabase
.from('threads')
.select('thread_title');
threadList.push(threads); // how do I get the current index of the loop (and limit it to only get 5 entries?)
threadList.forEach(function(value) {
console.log(value)
})
})();
setNewThreads(threadList);
}, []); // note the empty dependency list
The empty dependency list is one of the ways you know the effect will only run once.
To only add 5 of the items to the list, you can slice the result (or check to see if supabase has a builtin way of doing this):
threadList.push(threads.slice(0, 5));
Finally, note that state changes are asynchronous and more so in your case because you are actually doing a fetch over the network.
If you wanted to get notified when newThreads has been updated, you can use another useEffect like so:
useEffect(() => {
if (newThreads) {
console.warn(newThreads.length);
}
}, [newThreads]);
The if statement is not really needed, but added for completeness
This also applies for when you want to actually render the newThreads, you need to use conditional rendering to do that:
{
(Object.entries(newThreads) || []).map(([key, value]) => {
return (
<div className="widget-cell">
<div className={"widget-cell-image"} />
<div className="widget-cell-content">
<span className={"widget-cell-title"}>{value}</span>
<div className="widget-cell-values">
<span className={"widget-cell-by-user"}>by harley_swift,</span>
<span className={"widget-cell-time"}>22:02</span>
</div>
</div>
</div>
);
}) || <div>Loading...</div>
}

Related

Have a function return JSX, then setState

I'm new to React (and programming in general) and have come across an issue. I have created this component and using react-copy-to-clipboard package, onCopy I would like to copy a string, set copied to true and then after a couple of seconds set copied to false. When copied is true I want a div with some text to be displayed and when copied is set to false, I want it to disappear.
I have tried using setTimeout without success, as you can see in the code below and I suppose it's not working as I wish as the JSX doesn't re-render when the copied state is false again. I thought of using promises but couldn't figure out how to do that when I want to return JSX.
import React, { useState, useCallback } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import styled from 'styled-components';
const ClipBoard = () => {
const [copied, setCopied] = useState(false);
const onCopy = useCallback(() => {
setTimeout(setCopied(false), 2000)
setCopied(true);
console.log(copied)
}, [copied]);
return (
<div className="app">
<section className="section">
{copied ? <div><p>Copied</p></div> : null}
<CopyToClipboard onCopy={onCopy} text="Text I want to copy">
<button type="button">
<span className="sr-only">
E-mail
</span>
Text I want to copy
</button>
</CopyToClipboard>
</section>
</div>
);
}
export default ClipBoard;
Issue
The code is immediately invoking the state update to enqueue the copied state value to false.
setTimeout(
setCopied(false), // <-- immediately called!
2000
);
setTimeout expects a callback function that will be invoked when the timeout expires.
Solution
Pass an anonymous function to setTimeout to be called when the timeout expires. Since React state updates are enqueued and asynchronously processed, the console.log(copied) will only log the unupdated copied state value closed over in callback scope from the current render cycle. If you want to log state values use the useEffect hook. Also, you should consider the edge case where the component unmounts prior to the timeout expiring and clear any running timers.
Full Example:
const ClipBoard = () => {
const [copied, setCopied] = useState(false);
const timerRef = useRef(); // React ref for timer reference
useEffect(() => {
// Return cleanup function to clear timer when unmounting
return () => {
clearTimeout(timerRef.current);
};
}, []);
useEffect(() => {
// Effect to log any copied state updates
console.log(copied);
}, [copied]);
const onCopy = useCallback(() => {
timerRef.current = setTimeout( // save timer reference
() => setCopied(false), // function callback to update state
2000,
);
setCopied(true);
}, []); // empty dependency array
return (
<div className="app">
<section className="section">
{copied && <div><p>Copied</p></div>}
<CopyToClipboard onCopy={onCopy} text="Text I want to copy">
<button type="button">
<span className="sr-only">
E-mail
</span>
Text I want to copy
</button>
</CopyToClipboard>
</section>
</div>
);
}

Unable to update a list with react when its value is being changed using the hook useState function from React TypeError: map is not a function

I want to get a list of values updated whenever its value is changed through a hook setState function, however I am getting an error I don't know why... I am getting a .map is not a function TypeError
Down bellow is my code and I also have a codesandbox link: https://codesandbox.io/s/fervent-satoshi-zjbpg?file=/src/Incomes.js:23-1551
import axios from "axios";
import { useState, useEffect } from "react";
const fetchInvestment = async () => {
const res = await axios.get(
"https://6r3yk.sse.codesandbox.io/api/investments/60b2696de8be014bac79a2a1"
);
return res.data.invest.incomes;
};
export default function Incomes() {
const [incomes, setIncomes] = useState([]);
const [date, setDate] = useState(undefined);
const [value, setValue] = useState(undefined);
useEffect(() => {
const getInvestments = async () => {
const res = await fetchInvestment();
setIncomes(res);
};
if (incomes.length === 0) {
getInvestments();
}
console.log(incomes);
}, [incomes]);
return (
<div>
<h1>Hello CodeSandbox</h1>
<input
id="monthEl"
type="month"
value={date}
onChange={(e) => {
setDate(e.target.value);
}}
/>
<input
id="monthEl"
type="number"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button
onClick={() => {
const income = {};
income[date] = Number(value);
setIncomes(incomes.push(income));
setTimeout(() => {
console.log(incomes);
}, 2000);
}}
>
Save
</button>
<ul>
{incomes.map((income) => (
<li key={Object.keys(income)}>
{Object.keys(income)}: {Object.values(income)}
</li>
))}
</ul>
</div>
);
}
Replace this line:
setIncomes(incomes.push(income));
With this
setIncomes([...incomes, income]);
The .push method returns the length of the array, not the actual array. You can use the spread operator to spread the current array and then add on the new item to the end of it.
Doing this should also work:
incomes.push(incomes)
setIncomes(incomes)
It's possible you're getting that error because the data coming back from your API isn't an array. Judging by your code, I'm guessing you're expecting a key/value map, which in JS is an object. You might be able to use Object.keys(incomes).map(...), but without knowing the specific response format, I can't say for sure.
There are 2 other issues with your code:
First, you can't push onto incomes because it's a React state array. Instead, you need to use the setIncomes callback...
setIncomes([...incomes, income])
Additionally, the way you're using Object.keys and Object.values is not correct. Again, can't say what the correct way is without knowing specifics of your response format.

Object destructuring in map function

Below is the component I am working on:
//PURPOSE : Component to show some useful information about the current page. like LAST REFRESH TIME OF DATA.
//Props that needs to be passed. - {MessageTitle}
//
import React, { useEffect, useState } from "react";
import "../../css/BubbleInfoComponent.scss";
import ApiHelper from "../../api/ApiHelper";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
const BubbleInfoComponent = (props) => {
let [Info, setInfo] = useState({});
function onClicko() {
console.log("Handling Click");
}
useEffect(() => {
getData();
// eslint-disable-next-line
}, [Info]);
function getData() {
ApiHelper.GetLastRefreshTime(props.uid).then((response) => {
// const infoData = response.data.map(({sysName, lastRefreshTime}) =>{
// return (sysName ? <div key={sysName}>`{sysName}:{lastRefreshTime}`</div>): <div>{lastRefreshTime}</div>;
// })
// setInfo(infoData);
// console.log("infoData:- ")
// console.log(infoData);
});
}
return (
<>
<div id="Bubble__Circle" onClick={onClicko}>
<p>
<FontAwesomeIcon icon="info" color="#30343f" />
</p>
</div>
<div id="Bubble__Content">
<div id="Bubble__Content_Msg_Title">{props.MessageTitle}</div>
{/* <div id="Bubble__Content_Msg_Title">{Info}</div> */}
</div>
</>
);
};
export default BubbleInfoComponent;
There will be two kind of JSON response I will be getting:
{
"SystemA": "07-04-2021 08:00",
"SystemB": "07-04-2021 08:00",
"SystemC": "07-04-2021 08:00"
}
{"responses": "07-04-2021 08:00"}
What I want to implement is 1st type of response I want to set the value of Info in "Key": "Value" format and for 2nd type of response only time should be visible.
I hope I made my point clear. I know I did something wrong while destructuring in line21, this might look silly but being a newbie to JavaScript, I am not able to identify where i am going wrong.
Map function can be run on arrays and not objects. So the first thing which has to be done is to convert object into and array. For that you can use the Object.entries which will give you the key and values for each of the elements in the object.
Reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
Based on the response type you either access the time directly or run through a map. Here is the code to start with :
ApiHelper.GetLastRefreshTime(props.uid).then((response) => {
const mapSystemNames = (data) => Object.entries(data).map(([key,value]) => <div key={key}>{key}:{value}</div>)
const infoData = response.data.responses? <div>{response.data.responses}</div> : <div>{mapSystemNames(response.data)}</div>;
// setInfo(infoData);
// console.log("infoData:- ")
// console.log(infoData);
});
Probably there may be some edge case scenarios / type checks needed to make it more robust, but this could get you started in the right direction. Please let me know if you have any queries.

Re render child component depending on child action while passing data to parent in React

I have a Parent Component where I make an Ajax call to hydrate two children Components. C1 only needs the data once, C2 actually is able to get more data through additional Ajax calls and will be re render adequately .
It is way easier for me to make the Ajax Call in my Parent Component. But then I can't figure out how to 'get more data' in C2, pass that data in Parent and finally re render C2 only. I had many tries through callbacks (you can see it in the code), lifting state up etc, I am unable to do it. I don't know what to do with useEffect either...
Some code :
const Reviews: FunctionComponent = () => {
let variables = {
offset: 0,
limit: 2,
filter: filter,
order: order
}
useEffect(() => {
console.log("useEffect called");
});
const {data: dataReviews, loading: loadingReviews, error: errorReviews} = useQuery(GetReviews, {
ssr: false,
variables: variables
});
function parentCallback(offset: number, limit: number) {
variables.offset = offset;
variables.limit = limit;
}
if (!loadingReviews && !errorReviews && dataReviews) {
let reviews = !loadingReviews && !errorReviews && dataReviews ? dataReviews.reviews[0] : null;
const stats = reviews.stats;
reviews = reviews.reviews;
return (
<div className={`${styles.netreviews_review_rate_and_stars}`}>
<ReviewsSideInfo stats={stats}
total={getRecommandation(stats).total}
recommandation={getRecommandation(stats).percentageRecommandation}
/>
<ReviewsContainer parentCallback={parentCallback} reviews={reviews}/>
</div>
);
}
return (<div></div>);
}
export default Reviews;
As you can see <ReviewsSideInfo/> (C1) relies on that Ajax call (using stats object), and so does <ReviewsContainer /> (C2), but in C2 -> :
const ReviewsContainer: FunctionComponent<ReviewsContainerProps> = ({reviews, parentCallback}) => {
const [{offset, limit}, setReviews] = useState({offset: 0, limit: 3});
return (
<div className={`${styles.right_block}`}>
<div className={`${styles.reviews_list}`}>
<Fragment>
{reviews.map((element, i) => {
return (
<div>
<div key={i}>
<Review rate={element.rate}
firstname={element.firstname}
lastname={element.lastname}
review={element.review}
/>
</div>
</div>
)
})}
<button onClick={() => {
setReviews({offset: 0, limit: limit + 3});
parentCallback(0, limit + 3)
}}>
<FormattedMessage id="load-more"/></button>
</Fragment>
</div>
</div>
)
}
-> I need to be able to load more data.
Sorry for the long piece of code. How can I achieve that ? I know useReducer may be of help but it seems even more difficult to me since I'm new to React.
Last thing : in C1, I need to be able to filter that data (per rating) so I need to find a way to communicate through all these components... same problem actually.
EDIT: I could have updated the state in Parent component but then the whole component would refresh and that's not what I want (already tried that)
Did you try wrapping your component with
React.memo(your component)
I will look something like below:
const MemoisedComponent = React.memo(ReviewsSideInfo);
<div className={`${styles.netreviews_review_rate_and_stars}`}>
<MemoisedComponent stats={stats}
total={getRecommandation(stats).total}
recommandation={getRecommandation(stats).percentageRecommandation}
/>
<ReviewsContainer parentCallback={parentCallback} reviews={reviews}/>
</div>
Also in
parentCallback(offset,limit) {
setOffset(offset)
setLimit(limit)
}
below useEffect will only be called when the offset and limit value is changed.
useEffect(() => {
//api call logic
},[offset,limit]);
below is how your state should look like:
const [offset,setOffset] = useState(0)
const [limit,setLimit] = useState(3)
...and so on

Infinity Loop with promises and state variables in ReactJs

My react component is rendering multiple times in infinity loop. What am I missing?
Here is my code.
const DocViewer = ({ title, closeModal }) => {
const [docsSimilares, setDocsSimilares] = useState([]);
const baseUrl = '/docs'
async function similares() {
return await axios.get(`${baseUrl}/${title}`).then(data => setDocsSimilares(data.data.body.hits.hits[0]._source.documentos_similares))
}
similares().then(console.log(docsSimilares))
return (
<div class="row">
<div class="column pdf">
<h1>{title}</h1>
<PDFViewer url={sFileNameDemo} />
</div>
<div class="column semelhantes">
<button onClick={closeModal} >Fechar</button>
<p>{docsSimilares.map(doc => (
<div>
<p>{doc}</p>
<img alt={doc} src={doc} width="100%" />
</div>
))}</p>
</div>
</div>
);
}
export default DocViewer
When you call your similares() function you are setting state when calling setDocsSimilares.
By setting state you trigger re-render, when you trigger re-render it calls your similares() function again, that changes the state... You got the idea :)
By simply calling similares() inside of your functional component - it executes on each re-render.
You cannot just simply call similares() the way you do.
I think what you trying to achieve is to get data on first component mount, so you should use useEffect with empty array as second argument like so:
useEffect(() => similares().then(console.log(docsSimilares)),[])
If you are using hooks you need to understand useEffect
useEffect(() => similares().then(console.log(docsSimilares)),[])
The square brackets are where you add dependencies, if no dependencies it will only run on initializing "mounting" it is the same as componentDidMount() so that it will only fire the async request on load.
This code solved my problem:
async function similares() {
const simDocs = await axios.get(`${baseUrl}/${title}`).then(data => (data.data.body.hits.hits[0]._source.documentos_similares))
setDocsSimilares(simDocs)
}
useEffect(() => {
similares().then(data => console.log(data))
},[]);

Categories

Resources