Getting Data using query Params and Firebase with React - reactjs

I am trying to return a single Booklet from firestore whose ID matches the id from the URL. I am using useParams() to extract the id from the url. When I console.log singleBooklet, it is an empty Array. I am pretty sure the problem is that the component is rendering before the query in the useEffect has time to setSingleBooklet. Any solutions?
const Booklet = () => {
const [singleBooklet, setSingleBooklet] = useState([]);
const params = useParams();
useEffect(() => {
if (params.id) {
const q = query(
collection(db, "Booklet"),
where("bookletId", "==", params.id)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
setSingleBooklet(
querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
});
console.log(singleBooklet);
return unsubscribe;
}
}, []);
return (
<div className="booklet-section row">
<div className="col-md-8 col-sm-10 col-lg-7 col-xl-5 mx-auto">
<Carousel interval={null}>
<Carousel.Item>
<h4 className="text-center">Cover</h4>
<div className="cover">
<img src={ELFlogo} className="cover-logo"></img>
<h1 className="text-secondary">{singleBooklet[0].title}</h1>
<br></br>
<h5>By</h5>
<h3 className="text-secondary"></h3>
</div>
</Carousel.Item>
</Carousel>
</div>
</div>
);
};

Related

Rerender component on click

I'm trying to generate a new quote when the button is clicked. Having trouble figuring out how to implement this. Googling around has led me to believe that the useCallback hook is the way to go, but I haven't any experience with it so I have yet to have any luck implementing it. Any help is appreciated! Thank you in advance.
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useEffect, useState, useCallback } from 'react'
const Main = () => {
const [quote, setQuote] = useState(null)
const [author, setAuthor] = useState(null)
const [newQuote, setNewQuote] = useState(false)
useEffect(() => {
fetch('https://type.fit/api/quotes')
.then(response => response.json())
.then((data) => {
let randomIndex = Math.floor((Math.random() * data.length));
setQuote(data[randomIndex].text)
setAuthor(data[randomIndex].author)
})
.catch(err => console.error(err));
}, [])
return (
<div id='main' className='grid place-items-center h-screen w-screen text-center'>
{/* Quote Gen Container */}
<div className='flex flex-col justify-start mx-auto bg-sky-300 w-3/4 h-3/4 text-black space-y-3 p-32 rounded-3xl relative'>
<h1 className='text-bold text-3xl absolute top-0 mx-auto'>Random Quote Generator</h1>
<div>
<h4 id="text">{`"${quote}"`}</h4>
</div>
<div>
<p id="author">{`- ${author}`}</p>
</div>
<div id="button">
<button onClick={() => setNewQuote(null)} className='bg-black text-white rounded-xl p-2 abs'>New Quote</button>
</div>
</div>
</div>
)
}
export default Main
Refactor the fetch logic into a callback that can be called from either the useEffect hook or directly in a button's onClick handler.
const Main = () => {
const [quote, setQuote] = useState({});
// Memoize a stable callback function reference
const fetchQuote = useCallback(() => {
fetch('https://type.fit/api/quotes')
.then(response => response.json())
.then((data) => {
const randomIndex = Math.floor((Math.random() * data.length));
setQuote(quotes[randomIndex]);
})
.catch(err => console.error(err));
}, []);
// Initial fetch of quotes
useEffect(() => {
fetchQuote();
}, [fetchQuote]);
if (!quote) return null;
return (
<div id='main' className='....'>
{/* Quote Gen Container */}
<div className='....'>
<h1 className='....'>Random Quote Generator</h1>
<div>
<h4 id="text">{`"${quote.text}"`}</h4>
</div>
<div>
<p id="author">{`- ${quote.author}`}</p>
</div>
<div id="button">
<button
onClick={fetchQuote} // <-- attach callback to fetch quote
className='....'>
New Quote
</button>
</div>
</div>
</div>
)
}
If the fetched data doesn't change then fetch it once and select a random quote.
const Main = () => {
const [quotes, setQuotes] = useState([]);
const [quote, setQuote] = useState(null);
// Memoize function to set single random quote
const getQuote = useCallback(() => {
const randomIndex = Math.floor((Math.random() * quotes.length));
setQuote(quotes[randomIndex]);
}, [quotes]);
// Mounting useEffect to fetch and save all quotes
// and set initial random quote
useEffect(() => {
fetch('https://type.fit/api/quotes')
.then(response => response.json())
.then((data) => {
setQuotes(data);
const randomIndex = Math.floor((Math.random() * data.length));
setQuote(data[randomIndex]);
})
.catch(err => console.error(err));
}, []);
if (!quote) return null;
return (
<div id='main' className='....'>
{/* Quote Gen Container */}
<div className='....'>
<h1 className='....'>Random Quote Generator</h1>
<div>
<h4 id="text">{`"${quote.text}"`}</h4>
</div>
<div>
<p id="author">{`- ${quote.author}`}</p>
</div>
<div id="button">
<button
onClick={getQuote}
className='....'
>
New Quote
</button>
</div>
</div>
</div>
)
}
Assuming you don’t need to re-fetch the data every time you could store the returned data in state, then have your onclick just choose a new random entry to set as the “current” quote.

Use State not updating as expected

Fairly new to react and trying to build a clone of The Movie Database site. I want this toggle switch to change my api call from movies to tv. It starts working after a couple clicks, but then it throws everything off and it's not displaying the correct items anyway. Not really sure what's going on here...or even why it starts working after two clicks. Anyone know whats up with this?
import React, { useState, useEffect } from "react";
import axios from "axios";
import API_KEY from "../../config";
const Popular = ({ imageUri }) => {
// GET POPULAR MOVIES
const [popularMovies, setPopularMovies] = useState("");
const [genre, setGenre] = useState("movie");
console.log(genre);
const getPopular = async () => {
const response = await axios.get(
`https://api.themoviedb.org/3/discover/${genre}?sort_by=popularity.desc&api_key=${API_KEY}`
);
setPopularMovies(response.data.results);
};
useEffect(() => {
getPopular();
}, []);
const listOptions = document.querySelectorAll(".switch--option");
const background = document.querySelector(".background");
const changeOption = (el) => {
let getGenre = el.target.dataset.genre;
setGenre(getGenre);
getPopular();
listOptions.forEach((option) => {
option.classList.remove("selected");
});
el = el.target.parentElement.parentElement;
let getStartingLeft = Math.floor(
listOptions[0].getBoundingClientRect().left
);
let getLeft = Math.floor(el.getBoundingClientRect().left);
let getWidth = Math.floor(el.getBoundingClientRect().width);
let leftPos = getLeft - getStartingLeft;
background.setAttribute(
"style",
`left: ${leftPos}px; width: ${getWidth}px`
);
el.classList.add("selected");
};
return (
<section className="container movie-list">
<div className="flex">
<div className="movie-list__header">
<h3>What's Popular</h3>
</div>
<div className="switch flex">
<div className="switch--option selected">
<h3>
<a
data-genre="movie"
onClick={(e) => changeOption(e)}
className="switch--anchor"
>
In Theaters
</a>
</h3>
<div className="background"></div>
</div>
<div className="switch--option">
<h3>
<a
data-genre="tv"
onClick={(e) => changeOption(e)}
className="switch--anchor"
>
On TV
</a>
</h3>
</div>
</div>
</div>
<div className="scroller">
<div className="flex flex--justify-center">
<div className="flex flex--nowrap container u-overScroll">
{popularMovies &&
popularMovies.map((movie, idX) => (
<div key={idX} className="card">
<div className="image">
<img src={imageUri + "w500" + movie.poster_path} />
</div>
<p>{movie.title}</p>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default Popular;
You're using the array index as your key prop when you're mapping your array.
You should use an id that is specific to the data that you're rendering.
React uses the key prop to know which items have changed since the last render.
In your case you should use the movie id in your key prop instead of the array index.
popularMovies.map((movie) => (
<div key={movie.id} className="card">
<div className="image">
<img src={imageUri + 'w500' + movie.poster_path} />
</div>
<p>{movie.title}</p>
</div>
));
Also
You're calling the api directly after setGenre. However state changes aren't immediate. So when you're making your api call you're still sending the last movie genre.
Two ways of fixing this:
You could call your function with the genre directly, and change your function so it handles this case:
getPopular('movie');
Or you could not call the function at all and add genre as a dependency of your useEffect. That way the useEffect will run each time the genre change.
useEffect(() => {
getPopular();
}, [genre]);
PS: You should consider splitting your code into more component and not interacting with the DOM directly.
To give you an idea of what it could look like, I refactored a bit, but more improvements could be made:
const Popular = ({ imageUri }) => {
const [popularMovies, setPopularMovies] = useState('');
const [genre, setGenre] = useState('movie');
const getPopular = async (movieGenre) => {
const response = await axios.get(
`https://api.themoviedb.org/3/discover/${movieGenre}?sort_by=popularity.desc&api_key=${API_KEY}`
);
setPopularMovies(response.data.results);
};
useEffect(() => {
getPopular();
}, [genre]);
const changeHandler = (el) => {
let getGenre = el.target.dataset.genre;
setGenre(getGenre);
};
const isMovieSelected = genre === 'movie';
const isTvSelected = genre === 'tv';
return (
<section className="container movie-list">
<div className="flex">
<MovieHeader>What's Popular</MovieHeader>
<div className="switch flex">
<Toggle onChange={changeHandler} selected={isMovieSelected}>
In Theaters
</Toggle>
<Toggle onChange={changeHandler} selected={isTvSelected}>
On TV
</Toggle>
</div>
</div>
<div className="scroller">
<div className="flex flex--justify-center">
<div className="flex flex--nowrap container u-overScroll">
{popularMovies.map((movie) => {
const { title, id, poster_path } = movie;
return (
<MovieItem
title={title}
imageUri={imageUri}
key={id}
poster_path={poster_path}
/>
);
})}
</div>
</div>
</div>
</section>
);
};
export default Popular;
const Toggle = (props) => {
const { children, onChange, selected } = props;
const className = selected ? 'switch--option selected' : 'switch--option';
return (
<div className={className}>
<h3>
<a
data-genre="movie"
onClick={onChange}
className="switch--anchor"
>
{children}
</a>
</h3>
<div className="background"></div>
</div>
);
};
const MovieHeader = (props) => {
const { children } = props;
return (
<div className="movie-list__header">
<h3>{children}</h3>
</div>
);
};
const MovieItem = (props) => {
const { title, imageUri, poster_path } = props;
return (
<div key={idX} className="card">
<div className="image">
<img src={imageUri + 'w500' + poster_path} />
</div>
<p>{title}</p>
</div>
);
};

Why is my fetched data not appearing in my React hooks component?

I am trying to load data into my component for it to be displayed. I thought the issue was that I wasn't using async/await for the fetch, but even after adding that it still is not loading. I am logging out the "offerings" and it is just showing the empty array. How do I keep the component from rendering until after the data is loaded??
Thanks in advance!
const [offerings, setOfferings] = useState([]);
const loadData = async () => {
const res = await fetch(`http://52.207.83.69` + `${CRUD_OFFERING}`);
setOfferings(await res.json());
console.log(offerings, 'offerings')
};
useEffect(async () => {
navbarToggle();
await loadData();
}, []);
const dispatch = useDispatch();
const modalState = useSelector((state) => state.modal);
const modalToggle = () => {
dispatch({
type: MODAL_TOGGLE,
payload: !modalState.show,
});
ga.event("navbar_requestdemo_clicked");
};
const navbarOpenState = useSelector((state) => state.navbar);
const navbarToggle = () => {
if (!navbarOpenState.open) return;
dispatch({
type: NAVBAR_OPEN,
payload: false,
});
};
return (
<div
className="d-flex justify-content-center align-items-center bg-color-white fc-px-15"
onClick={navbarToggle}
>
<div className={homeStyles["padded-body"] + " col-11 p-0"}>
<div className=" position-relative bg-color-white">
<div className={homeStyles["img-holder"]}></div>
<div className="col-12 column position-absolute top-0 d-flex justify-content-center">
<div className="col-lg-6 col-12 fc-mt-2">
<SearchBar />
</div>
</div>
<div className="position-absolute top-50 translateY-middle">
<div className="position-relative">
<h1 className={`${homeStyles["hero-text"]} font-weight-bolder`}>
Building
<br />
Meaningful
<br />
Engagement
</h1>
<button
className="btn btn-primary-round mt-3 px-3 py-2"
onClick={() => {
modalToggle();
}}
>
Request access
</button>
</div>
</div>
</div>
<div
id={homeStyles["discover-section"]}
className="d-flex justify-content-center align-items-center"
>
<div className="col-12 column">
<h4 className="font-weight-bold">Discover</h4>
<div
id={homeStyles["offer-section"]}
className="row justify-content-center align-items-center"
>
{!offerings?.length &&
<h4 className="text-center">There are no active offerings.</h4>
}
</div>
<OfferingCarousal
offeringsList={offerings}
name={"Offerings"}
/>
<div id={homeStyles["consultancy-section"]} className="">
<div className="row">
<div
className="d-flex justify-content-center align-items-center col-lg-6 col-12 px-0 mt-3 mb-4"
id={homeStyles["consultancy-div"]}
>
<div className="col-12 column p-5">
<h1 className="font-weight-bold">Add your consultancy</h1>
<h5 className="mt-4">
Reach more people and organizations
</h5>
<Link href="/consultancies">
<button className="btn btn-primary-round mt-4">
Learn more
</button>
</Link>
</div>
</div>
<div className="col-lg-6 col-12 px-0">
<img
src="/images/Rachael_glasses_home_page.jpg"
id={homeStyles["consultant-img"]}
className="mt-3"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default HomeNew;
You can check if the data is present before you consume the data.
const [offerings, setOfferings] = useState([]);
const loadData = async () => {
const res = await fetch(`http://52.207.83.69` + `${CRUD_OFFERING}`);
setOfferings(res.json());
console.log(res, 'offerings')
};
useEffect(() => {
navbarToggle();
loadData(); // await has no affect inside useEffect for top level functions
}, []);
// removed internal code for clarity
const dispatch = useDispatch(...yourCode);
const modalState = useSelector(...yourCode);
const modalToggle = () => {...yourCode};
const navbarOpenState = useSelector(...yourCode);
const navbarToggle = () => {...yourCode};
// check after the hooks and before the consuming the data
if(!offerings && !offerings.length) return <>No offerings</>;
return <div>your side nav</div>;
};
It's also good practice to catch asynchronous errors as they occur to prevent your a single component form breaking your whole app. You can also take advantage of the try...catch and put in loading and error states too.
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [offerings, setOfferings] = useState([]);
const loadData = async () => {
try {
setError(false);
setLoading(true);
const res = await fetch(`http://52.207.83.69` + `${CRUD_OFFERING}`);
setOfferings(res.json());
} catch (e){
setError(true)
} finally {
setLoading(false);
}
};
// other code from above
if(error) return <>error</>;
if(loading) return <>loading</>;
if(!offerings && !offerings.length) return <>No offerings</>;
return <div>your side nav</div>;
};
async function can't be put in the useEffect hook directly.
https://prnt.sc/1lu7vdc
It can be like this.
useEffect(() => {
...
(async ()=>{
await loadData()
})();
}, []);
But in your case, I think you don't need to wait until loadData function is executed.
Just make sure you handle exceptions on the rendering for Empty data.

Update className with UseEffect hook

Currently i am practising my React skills. Now i am working on my Hooks knowledge. I have used a Coctail API to load some data. Just for fun i divided this up into three categories with a navigation bar.
When i click on the navigation items i go to the subcategories. But my classes won't update so the tab system doesn't work. You guys know what i have to do here? I tried this in a few different ways but none worked.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const OverView = () => {
const [ term, setTerm ] = useState('Gin');
const [ categorie, setCategorie ] = useState('Cocktail');
const [ debouncedTerm, setDebouncedTerm ] = useState(term);
const [ results, setResults] = useState([]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 10);
return () =>{
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const search = async () => {
const { data } = await axios({
'method':'GET',
'url':'https://the-cocktail-db.p.rapidapi.com/filter.php',
'headers':{
'content-type':'application/octet-stream',
'x-rapidapi-host':'the-cocktail-db.p.rapidapi.com',
'x-rapidapi-key':'49adf3a5ccmsh5d525b0005370d3p1b460bjsn7b0add17579e',
'useQueryString':true
},'params':{
'i': debouncedTerm
},
});
setResults(data.drinks);
};
search();
}, [debouncedTerm]);
const renderResults = results.map((result) => {
return (
<div key={result.idDrink} className="four wide column" style={{marginBottom:"20px"}}>
<a href={result.idDrink}>
<div className="card">
<img src={result.strDrinkThumb} width="250"/>
<div className="header">{result.strDrink}</div>
</div>
</a>
</div>
);
});
return (
<div className="ui grid">
<div className="row">
<h3>Select your favourate drink to get started</h3>
<div className="ui top attached tabular menu">
<div
className={'item active'}
onClick={(e) => setTerm('Gin') }>
Gin
</div>
<div
className={'item'}
onClick={(e) => setTerm('Vodka')}>
Vodka
</div>
<div
className={'item'}
onClick={(e) => setTerm('Whiskey')}>
Whiskey
</div>
</div>
</div>
<div className="row">
{renderResults}
</div>
</div>
);
};
export default OverView;
Thanks in advance.My first problem is that i don't know how to add an extra action upon my hook. I can't attach an extra onClick event and don't know how to add this to my useEfect functions.
Set the className based on the term state atom, simple as that.
I also refactored things a bit:
the result component is now, well, a separate component
searching is refactored into a separate function
import React, { useState, useEffect } from "react";
import axios from "axios";
const ResultCard = ({ result }) => (
<div className="four wide column" style={{ marginBottom: "20px" }}>
<a href={result.idDrink}>
<div className="card">
<img src={result.strDrinkThumb} width="250" />
<div className="header">{result.strDrink}</div>
</div>
</a>
</div>
);
async function doSearch(term) {
const { data } = await axios({
method: "GET",
url: "https://the-cocktail-db.p.rapidapi.com/filter.php",
headers: {
"content-type": "application/octet-stream",
"x-rapidapi-host": "the-cocktail-db.p.rapidapi.com",
"x-rapidapi-key": "49adf3a5ccmsh5d525b0005370d3p1b460bjsn7b0add17579e",
useQueryString: true,
},
params: {
i: term,
},
});
return data;
}
const OverView = () => {
const terms = ["Gin", "Vodka", "Whiskey"];
const [term, setTerm] = useState("Gin");
const [results, setResults] = useState([]);
useEffect(() => {
doSearch(term).then((data) => setResults(data.drinks));
}, [term]);
return (
<div className="ui grid">
<div className="row">
<h3>Select your favourate drink to get started</h3>
<div className="ui top attached tabular menu">
{terms.map((t) => (
<div
className={["item", term === t ? "active" : null].filter(Boolean).join(" ")}
onClick={(e) => setTerm(t)}
>
{t}
</div>
))}
</div>
</div>
<div className="row">
{results.map((result) => (
<ResultCard result={result} key={result.idDrink} />
))}
</div>
</div>
);
};
export default OverView;
You may want to look into the classnames module; the arcane [].filter().join() expression would become cx({item: true, active: t === term}) :)

React useState not triggering re-render of child component

I cant seem to figure out why my MovieDetailCard component will not re-render when the movie state changes. I am passing in the movie object. When I update the state the log outputs correctly in the useEffect but the MovieDetailsCard never receives the updated object.
const MovieDetails = () => {
const [movie, setMovie] = useState({});
const { id } = useParams();
const { poster } = useParams();
useEffect(() => {
const fetchMovie = async () => {
const response = await fetch(
`http://www.randyconnolly.com/funwebdev/3rd/api/movie/movies.php?id=${id}`
);
const data = await response.json();
setMovie({ ...data });
};
fetchMovie();
}, []);
useEffect(() => {
console.log(movie); // this successfully outputs when movie updates
}, [movie]);
return (
<div className="row">
<div className="col s12 m6">
<MovieDetailsCard poster={poster} movie={movie} /> // this does not update
</div>
<div className="col s12 m6">
<CastCrewCard />
</div>
</div>
);
};
Below is the MovieDeatailsCard. In the useEffect the console.log(movie) always returns null.
const MovieDetailsCard = ({ poster, movie }) => {
useEffect(() => {
console.log("in details");
console.log(movie);
}, []);
return (
<div className="card">
<div className="card-content">
<div className="row">
<div className="col s6">
<span className="card-title">Movie Title</span>
</div>
<div className="col s6 favouriteButton">
<FavouriteButton className="waves-effect waves-light btn">
<i className="material-icons">favorite</i>
</FavouriteButton>
</div>
</div>
<div className="row">
<div className="col s12 m6">
<img src={`https://image.tmdb.org/t/p/w342/${poster}.jpg`} alt="" />
</div>
<div className="col s12 m6">
<p>{movie.title}</p>
</div>
</div>
</div>
</div>
);
};
export default MovieDetailsCard;
Thanks guys for the input. This seems to be resolved now. before I was setting data by setData(data) but when I changed to setData({...data}) that worked!
By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.
refer: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
try updating useEffect dependendecy to include [id]:
useEffect(() => {
const fetchMovie = async () => {
const response = await fetch(
`http://www.randyconnolly.com/funwebdev/3rd/api/movie/movies.php?id=${id}`
);
const data = await response.json();
setMovie({ ...data });
};
fetchMovie();
}, [id]);
Are you sure that you are going receive data from API? The website does not shows up.
Try this to test the API
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json)
In the state declaration (useState) it is better to declare all the keys that you are going to receive all from the API

Resources