This is more of a curiosity question and I feel that it would be useful to know why, but can anyone explain why console.log(recipe) prints twice. When I click Search button the results prints twice in the console. I think i has to do with react re-rendering the component twice, can this be explained in detail.
function Search(props) {
const recipe = useSelector(state => state.recipe)
const dispatch = useDispatch()
const [query, setQuery] = useState("")
console.log(recipe)
const handleQuery = (event) => {
event.preventDefault();
console.log(`Query: ${query}`)
dispatch(fetchRequest(query))
}
return (
<form className={classes.Search} onSubmit={handleQuery}>
<input
className={classes.Search__field}
placeholder="Search over 1,000,000 recipes..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button className={[buttonClasses.Btn, "search__btn"].join(' ')} type="submit"
>
<svg className={"search__icon"}>
<use href={magnifyingGlass + "#icon-magnifying-glass"}></use>
</svg>
<span>Search</span>
</button>
</form>
);
}
export default Search;
Here is where the Search Component is being used
import React from 'react';
import Search from './Search/Search';
import classes from './Header.module.css';
import logo from '../../img/logo.png';
import Likes from '../Header/Likes/Likes';
const header = (props) => {
return (
<header className={classes.Header}>
<img src={logo} alt="Logo" className={classes.Header__logo} />
<Search />
<Likes />
</header>
)
}
export default header;
Header function is then being used in the Layout function which is
in App.js
import React, { Component } from 'react';
import Aux from '../../hoc/Aux';
import classes from './Layout.module.css';
import Header from '../Header/Header';
import Results from '../Results/Results';
class Layout extends Component {
render() {
return (
<Aux>
<Header />
<Results />
</Aux>
);
}
}
export default Layout;
Here is the redux action
import axios from 'axios';
const FETCH_REQUEST = 'FETCH_USERS_REQUEST'
export const fetchRecipe = (recipe) => {
return {
type: FETCH_REQUEST,
payload: recipe
}
}
export const fetchRequest = (query) => {
console.log(query)
return (dispatch) => {
axios(`https://forkify-api.herokuapp.com/api/search?q=${query}`)
.then(response => {
// console.log(response.data.recipes)
const recipe = response.data.recipes;
dispatch(fetchRecipe(recipe));
})
.catch(error => {
console.log(error)
})
}
}
Ciao, I'm not the maximum expert of react, but you could do a test. You know that useEffect hook is triggered every time component is re-rendered. So you could put your console.log in useEffect and see if will be logged twice. Something like:
useEffect(() => {
console.log(recipe);
})
If you got 2 logs, then it means that Search component is rendered twice. Otherwise could be something related to reactjs workflow and, as I said, I'm not so expert to explain why is logged twice.
Related
I´m getting an error when trying to render an image in component.
I paste the code here.
Is it possible that I need a babel or webpack plugin?
In this component, the image rendering works fine:
import React from "react";
function ProductItem({ product }) {
return product ? (
<div>
<div>
<img src={product.images[0]} alt={product.title} />
</div>
<div>
{product.title}
<br />
${product.price}
</div>
<p>{product.description}</p>
</div>
) : <p>Loading Product... </p>;
};
export default ProductItem;
In this other component is where I have the problem.
ProductDetail.js
import React from "react";
import useGetProducts from "../hooks/useGetProducts";
const API = 'https://api.escuelajs.co/api/v1/products';
function ProductDetail() {
const data = useGetProducts(`${API}/6`);
return (
<>
{data.products
?
<>
<h3>{data.products.title}</h3>
<p>{data.products.description}</p>
<div>
<img src={data.products.images[0]} alt="title" />
</div>
</>
: <h4>Loading...</h4>
}
</>
);
}
export default ProductDetail;
Custom Hook with useEffect, the useGetProducts function is responsible for bringing the data from the API with the Axios library
import { useEffect, useState } from "react";
import axios from "axios";
const useGetProducts = (API) => {
const [products, setProducts] = useState([])
const [error, setError] = useState("");
const [loaded, setLoaded] = useState(false);
useEffect(() => {
(async () => {
try {
const response = await axios(API);
setProducts(response.data);
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);
return { products, error, loaded };
};
export default useGetProducts
Your default state for products is [], so the conditional render data.products in ProductDetail.js always return true so you can change default state for products is null
const [products, setProducts] = useState(null);
The first answer is correct, so I will not duplicate it, but I see room for improvement in your code/example.
Your useGetProducts hook is very easy to break and hard to reuse. If you will pass the wrong URL or the structure of the API will change it will break your code. Also, the hook is not very generic, cause you will need to create similar fn for each entity. My suggestion. Use react-query and separate functions for calling API. So it will look like this.
import { useQuery } from 'react-query'
import axios from 'axios'
export default function ProductPage() {
const productResponse = useQuery('exchanges', () => getProduct('6'))
const { isLoading, isError, data: product } = productResponse
return (
<div>
{isLoading && <div>Loading...</div>}
{isError && <div>Something went wrong :(</div>}
{product && (
<div>
<h1>Product title: {product.title}</h1>
<p>
{product.images.map(imageSrc => (
<img key={imageSrc} src={imageSrc} alt="" />
))}
</p>
</div>
)}
</div>
)
}
interface Product {
id: string
title: string
images: string[]
}
function getProduct(id: string): Promise<Product> {
return axios
.get(`https://api.escuelajs.co/api/v1/products/${id}`)
.then(r => r.data)
}
PS. react-query requires additional configuration ( context provider, config, etc ). Please look into docs on how to use it.
I have a react app that has a "Bread Crumb Header" component, the data for this component comes from an API end point.
I use the bread crumb header component inside mulitiple components within the app, and based on the current path/window.location the bread crumb componet will get the data from the API and render the correct HTML via JSX.
The problem I have is when I navigate to diffent paths/window.location's within the application the bread crum component data doesn't update.
This is what the bread crumb component looks like:
import React, { useState, useEffect } from 'react';
import API from "../../API";
import { useLocation } from 'react-router-dom';
import { BreadCrumbTitleSection, SubtitleSection, Subtitle } from './breadCrumbHeaderStyle';
import { Breadcrumb } from 'react-bootstrap';
function BreadCrumbHeader() {
const location = useLocation();
const [breadCrumbData, setBreadCrumbData] = useState([]);
const getBreadCrumbData = async () => {
const breadCrumbHeaderResponse = await API.fetchBreadCrumbHeader(location.pathname);
setBreadCrumbData(breadCrumbHeaderResponse);
};
useEffect(() => {
getBreadCrumbData();
}, []);
return (
<div>
<BreadCrumbTitleSection backgroundUrl={breadCrumbData.BreadCrumbBgImage}>
<div className="container">
<div className="row no-gutters">
<div className="col-xs-12 col-xl-preffix-1 col-xl-11">
<h1 className="h3 text-white">{breadCrumbData.BreadCrumbTitle}</h1>
<Breadcrumb>
{breadCrumbData.BreadCrumbLinks.map(breadCrumbLink => (
<Breadcrumb.Item href={breadCrumbLink.LinkUrl} key={breadCrumbLink.Id} active={breadCrumbLink.IsActive}>
{breadCrumbLink.LinkText}
</Breadcrumb.Item>
))}
</Breadcrumb>
</div>
</div>
</div>
</BreadCrumbTitleSection>
<SubtitleSection>
<Subtitle> {breadCrumbData.SubTitle}</Subtitle>
</SubtitleSection>
</div>
);
}
export default BreadCrumbHeader;
and this is an example of how I am using it inside other components:
import React, { useContext } from 'react';
import { useParams } from "react-router-dom";
import { MenuContext } from '../context/menuContext';
import RenderCmsComponents from '../../components/RenderCmsComponents/';
import BreadCrumbHeader from '../../components/BreadCrumbHeader/';
import { CategorySection, CategoryContainer, CategoryItemCard, CategoryItemCardBody, CategoryItemCardImg, CategoryItemTitle, CategoryRow, AddToCartButton, ProductDescription} from './categoryStyle';
function Category() {
const [categoryItems] = useContext(MenuContext);
const { id } = useParams();
const category = categoryItems.find(element => element.CategoryName.toLowerCase() === id.toLowerCase());
var dynamicProps = [];
{
category && category.Products.map(productItem => (
dynamicProps.push(productItem.ProductOptions.reduce((acc, { OptionName, OptionsAsSnipCartString }, i) => ({
...acc,
[`data-item-custom${i + 1}-name`]: OptionName,
[`data-item-custom${i + 1}-options`]: OptionsAsSnipCartString
}), {}))));
}
return (
<div>
<BreadCrumbHeader /> << HERE IT IS
<CategorySection backgroundurl="/images/home-slide-4-1920x800.jpg" fluid>
<CategoryContainer>
<CategoryRow>
{category && category.Products.map((productItem, i) => (
<CategoryItemCard key={productItem.ProductId}>
<CategoryItemTitle>{productItem.ProductName}</CategoryItemTitle>
<CategoryItemCardBody>
<ProductDescription>{productItem.Description}</ProductDescription>
<div>
<CategoryItemCardImg src={productItem.ProductImageUrl} alt={productItem.ProductName} />
</div>
</CategoryItemCardBody>
<AddToCartButton
data-item-id={productItem.ProductId}
data-item-price={productItem.Price}
data-item-url={productItem.ProductUrl}
data-item-description={productItem.Description}
data-item-image={productItem.ProductImageUrl}
data-item-name={productItem.ProductName}
{...dynamicProps[i]}>
ADD TO CART {productItem.Price}
</AddToCartButton>
</CategoryItemCard>
))}
</CategoryRow>
</CategoryContainer>
</CategorySection>
<RenderCmsComponents />
</div>
);
}
export default Category;
I found this post on stack overflow:
Why useEffect doesn't run on window.location.pathname changes?
I think this may be the solution to what I need, but I don't fully understand the accepted answer.
Can someone breakdown to be how I can fix my issue and maybe give me an explaination and possible some reading I can do to really understand how hooks work and how to use them in my situation.
It seems that you should re-call getBreadCrumbData every time when location.pathname was changed. In the code below I've added location.pathname to useEffect dependency list
const location = useLocation();
const [breadCrumbData, setBreadCrumbData] = useState([]);
const getBreadCrumbData = async () => {
const breadCrumbHeaderResponse = await API.fetchBreadCrumbHeader(location.pathname);
setBreadCrumbData(breadCrumbHeaderResponse);
};
useEffect(() => {
getBreadCrumbData();
}, [location.pathname]); // <==== here
React newcomer here.
I'm loading Astronomy Picture of the Day in a component using a loading spinner.
I want the page to get data every time I call it from navbar but it's flashing old data before showing the spinner.
How to avoid this behavior? I don't want to use ComponentWillMount because it's deprecated and I'm using functions.
The component code:
import { useEffect, useContext } from 'react'
import { getApod } from '../context/nasa/NasaActions'
import NasaContext from '../context/nasa/NasaContext'
import Spinner from './layout/Spinner'
function Apod() {
const {loading, apod, dispatch} = useContext(NasaContext)
useEffect(() => {
dispatch({type: 'SET_LOADING'})
const getApodData = async() => {
const apodData = await getApod()
dispatch({type: 'SET_APOD', payload: apodData})
}
getApodData()
}, [dispatch])
const {
title,
url,
explanation,
} = apod
if (loading) { return <Spinner /> }
return (
<div>
<h2>{title}</h2>
<img src={url} className='apod' alt='apod'/>
<p>{explanation}</p>
</div>
)
}
export default Apod
Thanks for your time.
Edit: I deleted the repository. It's already answared correctly.
I suggest you another solution to keep your navbar clean.
You can declare an instance variable loaded using the useRef hook. This variable will be initialized to false and set to true as soon as the apod is dispatched to your store.
import { useContext, useRef } from 'react'
function Apod() {
const {apod, dispatch} = useContext(NasaContext)
const loaded = useRef(false);
const {title, url, explanation} = apod
useEffect(() => {
dispatch({type: 'SET_LOADING'})
const loadApod = async() => {
const apodData = await getApod()
loaded.current = true;
dispatch({type: 'SET_APOD', payload: apodData})
}
loadApod()
}, [dispatch])
if (!loaded.current) { return <Spinner /> }
return (
<div>
<h2>{title}</h2>
<img src={url} className='apod' alt='apod'/>
<p>{explanation}</p>
</div>
)
}
export default Apod;
I had an idea, to clean the object in Context using onClick on the navbar button.
Is this the best way? I don't know but it's working as I wanted.
import NasaContext from '../../context/nasa/NasaContext'
import { useContext } from 'react'
import { Link } from 'react-router-dom'
import logo from './assets/logo.png'
function Navbar() {
const {dispatch} = useContext(NasaContext)
const resetApod = () => {
const pathname = window.location.pathname
if ( pathname !== '/' ) {
dispatch({type: 'SET_APOD', payload: {}})
}
}
return (
<div className="navbar">
<div className="navbar-logo">
<img src={logo} alt='Experimentum'/>
</div>
<div className="navbar-menu">
<Link to='/' onClick={resetApod}>APOD </Link>
<Link to='/about'>ABOUT </Link>
</div>
</div>
)
}
export default Navbar
I am making a posts project with React. Whenever My home page just consists a bunch of posts in cards view. If I click on a certain card, it should navigate me to that post with some details. However, before the page renders completely, I still see some lines or 'unfinished' elements so to speak.
This is what I see before the post and it's description render completely
How can I fix this?
Here is my JS file with posts:
import React, { Component } from 'react'
import { Card } from "react-bootstrap";
import { Link } from "react-router-dom";
import './Posts.css'
export class Posts extends Component {
render() {
return (
<div>
{this.props.posts.map(post => (
<Link to={`/post/${post._id}`} key={post._id}>
<Card key={post._id} className="shadow-sm">
<Card.Img variant="top" src={post.image} />
<Card.Body>
<Card.Title id="cardtitle">{post.title}</Card.Title>
</Card.Body>
</Card>
</Link>
))}
</div>
)
}
}
And here is the Post detail:
import React, { useState, useEffect } from 'react'
import axios from "axios";
import { Media } from "react-bootstrap";
import './Post.css'
function Post({ match }) {
const [post, setPost] = useState({});
useEffect(() => {
const fetchPost = async () => {
const res = await axios.get(`${POST_URL}${match.params.id}`);
setPost(res.data);
}
fetchPost();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const POST_URL = `/api/v1/posts/`;
return (
<Media>
<img className="align-self-center mr-3 postImage" src={post.image} alt={post.title} />
<Media.Body>
<h5 className="postTitle text-center">{post.title}</h5>
<p>{post.description}</p>
</Media.Body>
</Media>
)
}
export default Post
You can create a function inside your component that conditionally render if the data has returned from the API. Something like this:
import React, { useState, useEffect } from 'react'
import axios from "axios";
import { Media } from "react-bootstrap";
import './Post.css'
function Post({ match }) {
const [post, setPost] = useState();
useEffect(() => {
const fetchPost = async () => {
const res = await axios.get(`${POST_URL}${match.params.id}`);
setPost(res.data);
}
fetchPost();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const POST_URL = `/api/v1/posts/`;
const renderPost = () => {
if (post) {
return(
<Media>
<img className="align-self-center mr-3 postImage" src={post.image} alt={post.title} />
<Media.Body>
<h5 className="postTitle text-center">{post.title}</h5>
<p>{post.description}</p>
</Media.Body>
</Media>
);
}
return <h1>Loading</h1>;
}
return (
renderPost();
)
}
export default Post
I recommend you to render a spinner or a placeholder to improve the user experience while the data is loading.
i'm trying to only render the component <IntercomClient /> after a user clicks "Accept" on a cookie consent banner. Clicking accept changes the GlobalLayout's intercomIsActive state to true and thereby renders the IntercomClient. This is working but the warning concerns me.
How can I workaround the child/parent state change? I've been looking around but don't really understand.
import React, { useState } from 'react'
import { CookieBanner } from '#palmabit/react-cookie-law'
import IntercomClient from '../components/intercomClient'
const GlobalLayout = ({ location, children }) => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
<IntercomClient active={intercomIsActive}/>
...
)}
IntercomClient
import React from 'react';
import Intercom from 'react-intercom'
const IntercomClient = ({ active }) => {
return active ? <div><Intercom appID="XXXXXX" /></div> : null
}
export default IntercomClient;
import React, {useState} from 'react';
const Example = () => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
<Layout>
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
<IntercomClient active={intercomIsActive}/>
...
</Layout>
);
};
export default Example;
import React, {useState} from 'react';
const Example = () => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
<Layout>
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
{
intercomIsActive &&
<IntercomClient active={intercomIsActive}/>
}
...
</Layout>
);
};
export default Example;