I'm working on an app with a login page and a main page. In my main page, I have a component A that accepts a request and posts it to a nodejs server, which updates it on a database. Another component B fetches data from the database and displays it. I want to be able to rerender component B when component A's request has been returned with a response.
I understand that I can create a dummy state variable and pass it to B and pass a prop function to A to allow it to change it, but is there a more effective way of doing this?
Code for A's get request:
const onSubmit = async (event) => {
event.preventDefault();
if (vendor === "" || amount === "") {
} else {
const res = await axios.post(`http://localhost:4000/newpayment`, {
vendor,
amount,
});
// want to check res and rerender component B
}
setVendor("");
setAmount("");
};
Code for component B:
import React, { useState, useEffect } from "react";
import axios from "axios";
const ComponentB= () => {
const [payments, setPayments] = useState([]);
const fetchPayments = async () => {
const res = await axios.get(`http://localhost:4000/paymentspending`);
setPayments(res.data);
};
useEffect(() => {
fetchPayments();
}, []);
const onClick = async (id) => {
await axios.post(`http://localhost:4000/markpaid`, { id });
fetchPayments();
};
const renderedPayments = Object.values(payments).map((payment) => {
return (
<div
className="card"
style={{ width: "100%", marginBottom: "20px" }}
key={payment.id}
>
<div className="card-body">
<h4>{payment.vendor}</h4>
<h5>{payment.amount}</h5>
<button
className="btn btn-success"
onClick={() => onClick(payment.id)}
>
Mark as Paid
</button>
</div>
</div>
);
});
return <div className="d-flex flex-row flex-wrap">{renderedPayments}</div>;
};
export default ComponentB;
There are several ways to share the state between components:
Lift the state up.
Add the global store to the app, for example, Redux or Recoil.
If it is a small app, lift the state. Use the Redux if it is a big project.
Related
I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}
The code below is a form in my react app which shows up when a user edits a record from a data table - datatable and edit part is not shown here as it's not relevant.
Explanation of Tab Mismatch issue :
As soon as the below page loads, I see the following tabs (image below) on the UI with Tab1 highlighted:
The network tab of my browser shows the following web service call:
First webservice call for Tab1:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=0
Similarly, when I click Tab 2, the web service call is :
Second webservice call for Tab2:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=1
And with Tab 3, it's :
Third webservice call for Tab3:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=2
As you can see, since I am sending the value of selectedTabIndex for the parameter assetCategoryId in my webservice call in the useEffect function and value of event.index for the parameter assetCategoryId inside onTabSelected function. , the Tab1 is highlighted because of 0 value of selectedTabIndex inside useEffect function and hence the
//Inside useEffect function
params: { assetCategoryId: selectedTabIndex }
//Inside onTabSelected function
params: {assetCategoryId: event.index}
However, since I have the following available:
const assetCategories = JSON.parse('[{"id":1,"value":"Tab 1"},{"id":2,"value":"Tab 2"},{"id":3,"value":"Tab 3"}]');
Question 1.
Inside useEffect function, I want to send first value of the id field to the assetCategoryId parameter instead of the selectedTabIndex while calling the webservice call. Is it possible to do this? I mean I did like this assetCategores[0].id and it worked but is this a good way to achieve this?
Question 2.
I want to keep the activeIndex value based on the first id parameter, which is 1 in my case and not based on selectedTabIndex like it is in my code now:
<TabView id='AssetsTabView' activeIndex={selectedTabIndex}
One another issue which could arise is when the values of id could be like this and I would still want to display Tab1 , Tab2 and Tab3 properly:
const assetCategories = JSON.parse('[{"id":34,"value":"Tab 1"},{"id":66,"value":"Tab 2"},{"id":999,"value":"Tab 3"}]');
My complete code below:
import React, { useState, useEffect } from 'react';
import { Form, withFormik} from 'formik';
import {Button} from '#material-ui/core'
import axios from 'axios'
import {TabPanel, TabView} from "primereact/tabview";
const RequestForm = (props) => {
const {values, setFieldValue, touched, errors, isSubmitting, handleReset, handleChange} = props;
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
useEffect(() => {
if(props.dataRequest){
let editeddataRequestId = props.dataRequest.requestId;
axios
.get('api/assets/search/getAssetCategories?requestId='+editeddataRequestId, {
params: {
assetCategoryId: selectedTabIndex
}
}).then(response => {
}).catch(err => console.log(err));
}//end if of props.dataRequest
}, []);
var onTabSelected = (event) => {
(event.index) ? setSelectedTabIndex(event.index) : null
if(props.dataRequest){
let editeddataRequestId = props.dataRequest.requestId;
axios
.get('api/assets/search/getAssetCategories?requestId='+editeddataRequestId, {
params: {
assetCategoryId: event.index
}
}).then(response => {
}).catch(err => console.log(err));
}
};
const assetCategories = JSON.parse('[{"id":1,"value":"Tab 1"},{"id":2,"value":"Tab 2"},{"id":3,"value":"Tab 3"}]');
const DynamicTabView = ({ activeIndex, onTabChange }) => (
<div style={{width: 'max-content', whiteSpace: 'nowrap', marginLeft: 'auto', marginRight: 'auto'}}>
<TabView id='AssetsTabView' activeIndex={selectedTabIndex}
onTabChange={(e) => setSelectedTabIndex(e.index), (e) => onTabSelected(e)}>
{assetCategories.map((item, i) =>
<TabPanel key={i} header={item.value}></TabPanel>
)}
</TabView>
</div>
);
return (
<div>
<Form className="form-column-3">
<DynamicTabView activeIndex={selectedTabIndex} onTabChange={(e) => {setSelectedTabIndex(e.index), () => { this.onTabSelected(e) }}}/>
<Button size="large" variant="contained" color="primary" onClick={props.onCancel} style={{marginLeft: '5px'}} type="button">Cancel</Button>
</Form>
</div>
)
};
export const DataRequestEnhancedFormEdw = withFormik({
mapPropsToValues: props => {
return {}
},
})(RequestForm)
import { useSelector, useDispatch } from "react-redux";
import { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io";
import axios from "axios";
import { fetchMovies } from "../../feautures/movies/moviesSlice";
import Rating from "../../components/UI/Rating/Rating";
import request from "../../requests";
import "./SingleMoviePage.scss";
import SimilarMovies from "../../components/SimilarMovies/SimilarMovies";
const SingleMoviePage = ({ match }) => {
const dispatch = useDispatch();
const [movieDetails, setMovieDetails] = useState({});
const [movieCredits, setMovieCredits] = useState({});
const history = useHistory();
console.log("single rendered")
// number month to string
const date = new Date(movieDetails.release_date);
const dateWithMonthName =
date.getFullYear() +
"-" +
date.toLocaleString("en-EN", { month: "long" }) +
"-" +
date.getDay();
/* params */
const movieId = match.params.id;
const page = match.params.page;
const genre = match.params.genre;
/* movies reducer handle */
const moviesStatus = useSelector((state) => state.movies.status);
/* base urls */
const baseImgUrl = "https://image.tmdb.org/t/p/original";
const movieDetailUrl = `https://api.themoviedb.org/3/movie/${movieId}?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
const movieCastUrl = `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
// go home page
const goHOme = () => {
history.goBack()
};
// fetch movie cast
useEffect(() => {
const fetchMovieCast = async () => {
let response = await axios.get(movieCastUrl);
response = response.data;
setMovieCredits(response);
};
fetchMovieCast();
}, [movieCastUrl]);
// fetch movie details
useEffect(() => {
const fetchMovieDetails = async () => {
let response = await axios.get(movieDetailUrl);
response = response.data;
setMovieDetails(response);
};
fetchMovieDetails();
}, [movieDetailUrl]);
let content;
if (moviesStatus === "loading") {
} else if (moviesStatus === "succeeded") {
content = (
<div
className="single-movie__container"
style={{
backgroundImage: `url(${
movieDetails.backdrop_path
? baseImgUrl + movieDetails.backdrop_path
: baseImgUrl + movieDetails.poster_path
})`,
}}
>
<div className="single-movie__details">
<IoMdArrowRoundBack
className="single-movie__back"
onClick={goHOme}
size={65}
color={"#e50914"}
/>
<h1 className="single-movie__title">{movieDetails.title}</h1>
<div className="single-movie__rate">
<Rating
rating={movieDetails.vote_average}
className="single-movie__stars"
/>
</div>
<p className="single-movie__overview">{movieDetails.overview}</p>
<div className="single-movie__informations single-movie__informations--genres">
<label className="single-movie__informations-heading">Genres</label>
<div className="single-movie__informations-container">
{movieDetails.genres?.map((genre) => {
return <div className="single-movie__info">{genre.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--stars">
<label className="single-movie__informations-heading">
Starring
</label>
<div className="single-movie__informations-container">
{movieCredits.cast?.slice(0, 4).map((star) => {
return <div className="single-movie__info">{star.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--released">
<label className="single-movie__informations-heading">
Release Date
</label>
<div className="single-movie__informations-container">
<div className="single-movie__info">{dateWithMonthName}</div>
</div>
</div>
<div className="single-movie__informations single-movie__informations--production">
<label className="single-movie__informations-heading">
Production
</label>
<div className="single-movie__informations-container">
{movieDetails.production_countries?.slice(0, 2).map((country) => {
return <div className="single-movie__info">{country.name}</div>;
})}
</div>
</div>
</div>
<SimilarMovies movieId={movieId} />
</div>
);
}
useEffect(() => {
if (genre === "POPULAR") {
dispatch(fetchMovies(request.fetchPopular(page)));
} else if (genre === "NOW PLAYING") {
dispatch(fetchMovies(request.fetchNowPlaying(page)));
} else if (genre === "UP COMING") {
dispatch(fetchMovies(request.fetchUpComing(page)));
}
}, [dispatch, genre, page]);
return <div className="single-movie">{content}</div>;
};
export default SingleMoviePage;
Hi all.When i clicked Card compenent it navigate me to the SingleMoviePage component.But SingleMoviePage component re-render five times.How can i find this issues's source ? And how can i prevent that ? Finally is there any problem to fetch MovieCast and MovieDetails in same useEffect hook ?
github repo : https://github.com/UmutPalabiyik/hope-movie-app
demo : https://hope-movie.web.app/page/1
The first 2 useEffect hooks fetch data separately which then update your local states, which then triggers re-rendering.
If you don't want to re-render after each data fetch (state update), I'd suggest adding a loading state. Set it to true first and return a loading icon as your content. Then after both movieDetails and movieCredits have been populated, set it to false and return the actual content. This should render twice in total.
Have you considered graphql? GraphQL can combine your api calls into one and it also handles loading state and error state.
Whatever solution you have, re-rendering will happen when you are fetching data. Initial render won't have any data and after fetching data it must re-render.
You should use only one useEffect hook your code is running for all three. React will handle the rest itself.
I'm trying to set components with 3 functionalities. Displaying PokemonList, getting random pokemon and find one by filters. Getting random pokemon works great but since 2 days I'm trying to figure out how to set pokemon list feature correctly
Below full code from this component.
It's render when click PokemonsList button inside separate navigation component and fire handleGetPokemonList function in provider using context.
The problem is that I can't manage rerender components when PokemonList is ready. For now i need to additionally fire forceUpadte() function manually (button onClick = () => forceUpdate())
I tried to use useEffect() in PokemonList component but it didn't work in any way.
I was also sure that after fetching data with fetchData() function I can do .then(changeState of loading) but it didn't work also.
What Am I missing to automatically render data from fetch in provider in PokemonList component? I'm receiving error about no data exist but if I use forceUpdate then everything is ok
Complete repo here: https://github.com/Mankowski92/poke-trainer
handleGetPokemonList function in provider below
const handleGetPokemonList = () => {
setCurrentPokedexOption('pokemonList');
async function fetchData() {
setImgLoaded(false);
let res = await fetch(`${API}?offset=0&limit=6/`);
let response = await res.json();
response.results.forEach((item) => {
const fetchDeeper = async () => {
let res = await fetch(`${item.url}`);
let response = await res.json();
let eachPoke = {
id: response.id,
name: response.name,
artwork: response.sprites.other['officialartwork'].front_default,
stats: response.stats,
};
fetchedPokemons.push(eachPoke);
};
fetchDeeper();
});
setPokemonList(fetchedPokemons);
if (fetchedPokemons) {
return setLoading(false);
}
}
fetchData()
.then((res) => setLoading(res))
.catch((err) => console.log('error', err));
};
PokemonList component below
import React, { useContext, useState, useCallback } from 'react';
import { StyledPokemonListContainer } from './PokemonList.styles';
import { PokemonsContext } from '../../../providers/PokemonsProvider';
const PokemonList = () => {
const ctx = useContext(PokemonsContext);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const { handleSetImgLoaded } = useContext(PokemonsContext);
return (
<>
{ctx.currentPokedexOption === 'pokemonList' ? (
<StyledPokemonListContainer>
{ctx.pokemonList && ctx.pokemonList.length ? (
ctx.pokemonList.map((item, i) => (
<div className="each-pokemon-container" key={i}>
<div className="poke-id">{item.id}</div>
<div className="poke-name">{item.name}</div>
<img className="poke-photo" onLoad={() => handleSetImgLoaded()} src={item ? item.artwork : ''} alt="" />
</div>
))
) : (
<div className="render-info">Hit rerender button</div>
)}
{/* {ctx.pokemonList ? <div>{ctx.pokemonList[0].name}</div> : <div>DUPPSKO</div>} */}
<div className="buttons">
<button onClick={() => console.log('PREVOIUS')}>Previous</button>
<button className="rerender-button" onClick={() => forceUpdate()}>
RERENDER
</button>
<button onClick={() => console.log('NEXT')}>Next</button>
</div>
</StyledPokemonListContainer>
) : null}
</>
);
};
export default PokemonList;
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import firebase from '../../config/firebaseConfig'
import SingleEventSummary from './SingleEventSummary'
import { getEvent } from "./../../store/actions/eventActions";
import "./SingleEvent.css";
const SingleEvent = props => {
const id = props.match.params.id;
const [eventItem, seteventItem] = useState([]);
const [isFavourite, setIsFavourite] = useState("no");
//getting specific event
useEffect(() => {
const db = firebase.firestore().collection('newEvents').doc(id)
db.onSnapshot(snapshot => {
seteventItem(snapshot.data())
})
}, [id])
//checking if there is favourite
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.get().then(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
},[isFavourite])
//setting as favourites
const favouriteClick = (uid, eid) => {
debugger;
let initFav = firebase.firestore().collection('users').doc(uid);
initFav.get().then(snapshot => {
const arrayUnion = firebase.firestore.FieldValue.arrayUnion(eid);
initFav.update({
favorites: arrayUnion,
});
setIsFavourite("yes")
},(error) => console.error(error))
}
//remove favourites
const removeFavourite = () => {
debugger;
const initFavo = firebase.firestore().collection('users').doc(props.auth.uid);
initFavo.get().then(snapshot => {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
let data = snapshot.data().favorites.filter(el => el != id )
initFavo.update({
favorites: data,
});
setIsFavourite("no")
}
}
},(error) => console.error(error))
return () => initFavo()
}
console.log("wtf is this shit", isFavourite)
if (isFavourite == "no") {
return (
<main className="single-event_main">
<a className="waves-effect waves-light btn" onClick={favouriteClick(props.auth.uid, id)}>Add As Favourites!!</a>
</main>
);
}
else {
return (
<main className="single-event_main">
<div className="row">
<div className="col s6">
<a className="waves-effect waves-light btn" disabled>Favourite Event!!</a>
</div>
<div className="col s6">
<a className="waves-effect waves-light btn" onClick={removeFavourite}>Remove From Favourites!!</a>
</div>
</div>
</main>
);
}
};
export default SingleEvent;
I am trying to set the value in hook, comparing if the event id exists in the user's database(if he/she has set that event as a favourite).
....
const [isFavourite, setIsFavourite] = useState("no");
//checking if there is favourite
useEffect(() => {
debugger;
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
debugger;
if(snapshot.data()) {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
setIsFavourite("yes")
}
else if(!snapshot.data().favorites.includes(id)){
setIsFavourite("no")
}
}
}
}, (error) => console.error(error));
return () => db()
},[])
....
The issue is, react goes inside both conditions endlessly setting the hook value both yes and no. Been stuck on this hours.
Any kind of help will be much appreciated!!!
jus offering a little refactor -> this is just a bit easier to read
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
return () => db()
},[])
I can't see why your code would be looping perhaps we need more code as the above commenter mentioned.
Ok now that you have shown us more code. I can say with a large degree of confidence it is because you are calling favouriteClick in the onClick of the "Add As Favourites" button.
Which is causing a weird loop.
Change
onClick={favouriteClick(props.auth.uid, id)
to
onClick={() => favouriteClick(props.auth.uid, id)
You are welcome!
you should have a stop condition for this hook, useEffect hook is triggered every time you render something, so you ending up changing props and rendering and then trigger useEffect which change props and trigger render lifecycle Hook.
You should have something like that
useEffect(() => {
// call database
},[setFavorite]) // here goes stop condition for useEffect
If setFavorite is still false it won't trigger trigger again, or if setFavorite is false and request from db is setting it to true then next time if you it's trigger useEffect again and setFavorite is still true then useEffect won't execute.
For more details read officials documentation https://reactjs.org/docs/hooks-effect.html
If you are updating firebase in setIsFavourite() then you are creating a change that will be picked up by the .onSnapshot listener. This will force an endless loop of : condition triggers change > listens for condition > condition triggers change > ad Infinitum.
You either need to switch from .onSnapshot to a one-off .get listener or you need to add a condition to prevent changes from propagating in this case. This custom condition will be specific to your implementation, not really something we can help with unless you show use more code and help us understand what you are trying to achieve (that should be a separate question though). So I suspect the former in this case.