I'm new in using React Context and Hooks in my project. Currently I'm facing a problem when my item in the component doesn't display on the screen when it initially load but the item does display on the screen when I clicked on some button.
I did do some debugging using console.logand in the console, it did shows my data, but on the screen, it shows nothing. The weird part is, when I clicked on any button on the screen, it finally show something on the screen.
Here is my code, in the OrderContext, I get all my data from Firestore.
//OrderContextProvider.js
import React, { createContext, useState, useEffect } from "react";
import Firebase from "../component/misc/firebase";
export const OrderContext = createContext();
const OrderContextProvider = props => {
const [userLocation, setUserLocation] = useState({
shop: "XXXXXXX"
});
const [food] = useState([]);
useEffect(() => {
userLocation.shop != null
? Firebase.firestore()
.collection("restaurants")
.doc(userLocation.shop)
.collection("foods")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
food.push(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
})
: console.log("User location unknown, please insert the location!");
}, [food]);
return (
<OrderContext.Provider value={{ userLocation, food }}>
{props.children}
</OrderContext.Provider>
);
};
export default OrderContextProvider;
and in Foods component, I tried get food from OrderContext and display <Foods/> in <Home/>
//foods.js
import React, { useEffect, useContext } from "react";
import { OrderContext } from "../../context/OrderContextProvider";
import { Grid } from "#material-ui/core";
const Foods = () => {
const { food } = useContext(OrderContext);
useEffect(() => {
console.log(food);
}, []);
return food.map((foods, index) => {
return (
<Grid key={index} item xs={6} sm={6} md={3} xl={3} className="food">
{foods.name}
</Grid>
);
});
};
export default Foods;
//foods.js
<Grid container className="container">
<Foods/>
</Grid>
May I know what is my mistake or what I missed out here?
Sorry for my bad English and thanks for reading
It's still unclear as to where/how you are using the OrderContextProvider component. But, one possible problem I see in the code is the way you are updating the "food" array. The way you are pushing items into it won't necessarily trigger the updates since you are not updating the state. You might want to do something like -
const OrderContextProvider = props => {
....
const [food, setFood] = useState([]);
useEffect(() => {
...
.then(function(querySnapshot) {
let newFoods = [];
querySnapshot.forEach(function(doc) {
newFoods.push(doc.data());
});
setFood([...food, ...newFoods]);
})
...
});
...
};
const food = [];
useEffect(() => {
userLocation.shop != null
? Firebase.firestore()
.collection("restaurants")
.doc(userLocation.shop)
.collection("foods")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
food.push(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
})
: console.log("User location unknown, please insert the location!");
},[food]);
You have not passed second argument to the useEffect that tells the hook to run for the first time it loads.
I guess this is the reason why it's not showing the data on the first load.
Related
I'm fairly new to React development and hope someone can help me with this problem. I'm coding along with a YouTube video https://www.youtube.com/watch?v=XtMThy8QKqU&t=10138s (2:55:00 shows what it is supposed to do)and for some reason I can't find the mistake I'm making. When I test my app on localhost the window in which the trailer is supposed to play is only displayed when I click certain movie covers but not when I click on others. my other problem is that it will never actually play a trailer. The console displays the error you can hopefully see here [1]: https://i.stack.imgur.com/vC6Sh.jpg
import movieTrailer from "movie-trailer";
import React, { useEffect, useState } from "react";
import YouTube from "react-youtube";
import axios from "./axios";
import "./Row.css"
const base_url = "https://image.tmdb.org/t/p/original/";
function Row({ title, fetchUrl, isLargeRow }) {
const [movies, setMovies] = useState([]);
const [trailerUrl, setTrailerUrl] = useState("");
//A snippet of code which runs based on a specific condition or variable
useEffect(() => {
// if brackets are blank [] it means run once when row loads, and don't run again
async function fetchData() {
const request = await axios.get(fetchUrl);
// console.log(request.data.results);
setMovies(request.data.results)
return request;
// async function fetchData() {
// try{
// const request = await axios.get(fetchUrl);
// console.log(request);
// return request;
// }
// catch (error){
// console.log(error);
// }
}
fetchData();
}, [fetchUrl]);
const opts = {
height: '390',
width: '100%',
playerVars: {
// https://developers.google.com/youtube/player_parameters
autoplay: 1,
},
};
//console.log(movies);
const handleClick = (movie) => {
if (trailerUrl){
setTrailerUrl('');
} else {
movieTrailer(movie?.name || "")
.then ((url) => {
const urlParams = new URLSearchParams(new URL(url).search);
setTrailerUrl(urlParams.get("v"));
}).catch(error => console.log(error));
}
};
return(
<div className="row">
<h2>{title}</h2>
<div className="row__posters">
{movies.map(movie => (
<img
key={movie.id}
onClick={() => handleClick(movie)}
className= {`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`${base_url}${isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name}/>
))}
</div>
{trailerUrl && <YouTube videoId="{trailerUrl}" opts={opts} /> }
</div>
)
}
export default Row
Invalid property name in movie
Taking a look at the tmdb docs it will show you what the properties of each object has. In this case, there is no name. Try using something like movie.title
In your handleClick() function you could use movie?.title.
Trying to use movie.name will give back a null value. Which errors out movieTrailer() and you get no YouTube url back.
Create handle function like this and the call it in your return function and use however you want... mainly should be used by using onClick method
I want to only show the "Load More" button when I have extra documents to show on my React and Firebase website.
Right now, I'm fetching only 2 documents from a firestore and I want to show the "Load More" button when I have more than 2 documents in my firestore. If I only have 2 or fewer than 2 documents in my firestore, I don't want to show the "Load More" button.
And I want to hide the "Load More" button after fetching all the documents that I have on the firestore.
Anyone, please help me with this!
useCollection Hook:
import { useEffect, useRef, useState } from "react"
// firebase import
import {
collection,
getDocs,
limit,
onSnapshot,
orderBy,
query,
startAfter,
where,
} from "firebase/firestore"
import { db } from "../firebase/config"
export const useCollection = (c, _q, _l, _o) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const q = useRef(_q).current
const o = useRef(_o).current
useEffect(() => {
let ref = collection(db, c)
if (q) {
ref = query(ref, where(...q))
}
if (o) {
ref = query(ref, orderBy(...o))
}
if (_l) {
ref = query(ref, limit(_l))
}
const unsubscribe = onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
setDocuments(results)
setIsLoading(false)
setError(null)
})
// unsubscribe on unmount
return unsubscribe
}, [])
const fetchMore = async (doc) => {
setIsLoading(true)
const q = query(
collection(db, c),
orderBy(...o),
startAfter(doc.createdAt),
limit(_l)
)
const newDocuments = await getDocs(q)
updateState(newDocuments)
setIsLoading(false)
}
const updateState = (docs) => {
if (!docs.empty) {
const tempPosts = []
docs.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
})
})
setDocuments([...documents, ...tempPosts])
}
}
return { documents, fetchMore, error, isLoading }
}
SolutionComments.js (In this file I'm showing the "Load More Comments" button)
import React, { useState } from "react"
import { useParams } from "react-router-dom"
import { useCollection } from "../../hooks/useCollection"
import Comment from "./Comment"
import CommentForm from "./CommentForm"
const SolutionComments = () => {
const [activeComment, setActiveComment] = useState(null)
const { id } = useParams()
const { documents, fetchMore, isLoading } = useCollection(
`solutions/${id}/comments`,
null,
2,
["createdAt", "desc"]
)
const fetchMoreComments = () => {
fetchMore(documents[documents.length - 1])
}
return (
<div className="mt-10">
<CommentForm docID={id} />
<div>
{documents &&
documents.map((comment) => (
<Comment
key={comment.id}
comment={comment}
replies={comment.replies}
activeComment={activeComment}
setActiveComment={setActiveComment}
/>
))}
</div>
{documents.length > 2 && (
<button onClick={fetchMoreComments} className="text-white bg-purple-500">
{!isLoading ? "Load More Comments!" : "Loading..."}
</button>
)}
</div>
)
}
export default SolutionComments
Firestore does not have a mechanism for automatically telling you how many documents are in a query. You will need to manage that aggregate count yourself. Then you could fetch that aggregate count and use that to determine whether to show a load more button or not.
However this is harder than it sounds. And most modern apps we don't use load more buttons rather we use infinite scroll.
To be honest, it has never been efficient or smart to show a result set count in an app. The fact that people have done it in the past doesn't mean that it is the right thing to do today. It might have made sense when you had small databases, typically running off of a desktop database. But in a cloud-based solution with millions of documents and millions of users, and complex queries, knowing how many documents are in a result set is a very hard problem to solve.
Here Below my code I would like to retrieve all data before starting the render of my component, is there any way to do that in react ? I guess it's maybe a simple code line but as I'm new in coding I still don't know all react components behavior. Thanks for your answer.
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
// import material ui
import CircularProgress from "#mui/material/CircularProgress";
import Box from "#mui/material/Box";
// import config file
import { SERVER_URL } from "../../configEnv";
const Products = ({ catList }) => {
// catList is data coming from app.js file in format Array[objects...]
console.log("catList ==>", catList);
const [isLoading, setIsLoading] = useState(true);
const [dataSku, setDataSku] = useState([]);
console.log("datasku ==>", dataSku);
const tab = [];
useEffect(() => {
// Based on the catList tab I fetch additionnal data linked with each object of catList array
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
// The result I receive from the call is an array of objects that I push inside the Tab variable
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}, [catList]);
return isLoading ? (
<Box sx={{ display: "flex" }}>
{console.log("there")}
<CircularProgress />
</Box>
) : (
<div className="products-container">
<div>LEFT BAR</div>
<div>
{dataSku.map((elem) => {
return (
<div>
<h2>{elem.name}</h2>
</div>
);
})}
</div>
</div>
);
};
export default Products; ```
#Jessy use your loading state to fetch data once,
In your useEffect, check for loading,
useEffect(() => {
if(loading) {
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}
}, [catList]);`
I finally managed to displayed all results by adding this condition on the isLoading
if (tab.length === catList.length) {
setIsLoading(false);
}
Many thanks guys for your insight :)
When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.
Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?
import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';
import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';
const Details = () => {
const [ countryData, setCountryData ] = useState(null);
let country;
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [])
return (
<>
<Container className="details">
<Row>
<Col sm={6}>
<DetailImage />
</Col>
<Col sm={6}>
<DetailInfo
name={countryData.name}
population={countryData.population}
region={countryData.region}
subRegion={countryData.subRegion}
capital={countryData.capital}
topLevelDomain={countryData.topLevelDomain}
currencies={countryData.currencies}
language={countryData.language}
/>
</Col>
</Row>
</Container>
</>
)
}
export default Details;
The child component below......
import React from 'react';
const DetailInfo = (props) => {
const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
return (
<>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
)
}
export default DetailInfo;
Ultimately, the problem comes down to not handling the intermediate states of your component.
For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.
const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
status: "loading",
data: null,
error: null
});
useEffect(() => {
let unsubscribed = false;
fetchData()
.then((response) => {
if (unsubscribed) return; // unsubscribed? do nothing.
setDataInfo({
status: "fetched",
data: response.data,
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
console.error('Failed to fetch remote data: ', err);
setDataInfo({
status: "error",
data: null,
error: err
});
});
return () => unsubscribed = true;
}, []);
switch (dataInfo.status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {dataInfo.error.message}
</div>
);
}
// render data using dataInfo.data
return (
/* ... */
);
If this looks like a lot of boiler plate, there are useAsyncEffect implementations like #react-hook/async and use-async-effect that handle it for you, reducing the above code to just:
import {useAsyncEffect} from '#react-hook/async'
/* ... */
const {status, error, value} = useAsyncEffect(() => {
return fetchData()
.then((response) => response.data);
}, []);
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {error.message}
</div>
);
}
// render data using value
return (
/* ... */
);
Because state only update when component re-render. So you should put console.log into useEffect to check the new value:
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL).then(res => {
console.log(res.data[0]);
setCountryData(res.data[0]);
});
}, []);
useEffect(() => {
console.log(countryData);
}, [countryData]);
useState does reflecting its change immediately.
I think that it would be probably solved if you set countryData to second argument of useEffect.
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [countryData])
The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.
import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";
const Details = () => {
const [countryData, setCountryData] = useState({});
let country = "USA";
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
console.log(country);
queryURL += country;
console.log(queryURL);
axios
.get(queryURL)
.then((res) => {
console.log(res.data[0]);
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData);
});
}, []);
return (
<Container className="details">
<Row>
<Col sm={6}>
<DetailInfo
name={countryData?.name}
population={countryData?.population}
region={countryData?.region}
subRegion={countryData?.subRegion}
capital={countryData?.capital}
language={countryData?.language}
/>
</Col>
<Col sm={6}></Col>
</Row>
</Container>
);
};
export default Details;
Checkout my Codesandbox here for an example.
Im having troubles rendering components based on api calls in React. I fetch my data in useEffect hook update a state with the data. The state is null for a while before the api get all the data but by that time, the components are rendering with null values. This is what I have:
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
//if found is 0 not loaded, 1 is found, 2 is not found err
const [found, setFound] = useState(0);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
setFound(1);
})
.catch(err => {
console.log(err.message);
setFound(2);
});
}, [])
if(found===2) {
return(
<Redirect to="/" push />
)
}else{
console.log(poll)
return (
<div>
</div>
)
}
}
export default Poll
That is my workaround but it doesnt feel like thats the way it should be done. How can I set it so that I wait for my api data to get back then render components accordingly?
You don't need to track the state of the API call like const [found, setFound] = useState(1). Just check if poll exists and also you can create a new state variable for tracking the error.
For example if (!poll) { return <div>Loading...</div>} this will render a div with 'loading...' when there is no data. See the code below, for complete solution,
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [hasError, setHasError] = useState(false);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setHasError(true)
});
}, [])
if(!poll) {
console.log('data is still loading')
return(
<div>Loading....</div>
)
}
if (hasError) {
console.log('error when fetching data');
return (
<Redirect to="/" push />
)
}
return (
<div>
{
poll && <div>/* The JSX you want to display for the poll*/</div>
}
</div>
);
}
export default Poll
In your than, try to use a filter:
setPoll(poll.filter(poll => poll.id !== id));
Make sure to replace id by your identificator
The standard way is to have other variables for the loading and error states like this
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setError(true);
})
.finally(()=> {
setLoading(false);
};
}, [])
if(error) return <span>error<span/>
if(loading) return <span>loading<span/>
return (
<div>
// your poll data
</div>
)
}