React Component State and API fetch - reactjs

I have been studying React for past few days. In my blog project, I am using Axios to get data from API. Here is my component:
import React, { useState, useEffect } from "react";
import axios from "axios";
import { apiConstants } from "../../constants";
import SinglePost from "./SinglePost";
const PostContent = props => {
const {
match: { params }
} = props;
const [post, setPost] = useState({});
useEffect(() => {
axios
.get(apiConstants.singlePost + `${params.post_slug}`)
.then(function(response) {
setPost(response.data);
})
.finally(function() {
// always executed
});
}, []);
return (
<React.Fragment>
<div className="container">
<div className="row">
<div className="col-lg-8 col-md-10 mx-auto">
<SinglePost post={post} />
</div>
</div>
</div>
</React.Fragment>
);
};
export default PostContent;
Above code works fine though I noticed the first time it tries to render the component with empty ({}) post object (Due to the default value in 'useState'). However, it causes issues in my child component cause it is directly using 'post' object properties. For example: 'post.content'. Here is my 'SinglePost' component's code:
const SinglePost = props => {
const { post } = props;
console.log(post);
return (
<div>{post.content}</div>
);
};
It returns undefined error for {post.content} object. To resolve the issue I had to use something like {post && <SinglePost post={post} />}, But it doesn't feel right. Is there any better way to handle such scenarios.

Consider revising the PostContent component's rendering logic to account for the case where no post data is present during the network request.
You could for instance initialise your post state to null, and then update the rendered result to prevent the SinglePost component from being rendered while post is null.
Once the network request is completed and the post state is set, the component will re-render causing SinglePost to be rendered with the non-null post state:
const PostContent = props => {
const {
match: { params }
} = props;
const [post, setPost] = useState(null); /* Update: "Initial data" set to null */
useEffect(() => {
axios
.get(apiConstants.singlePost + `${params.post_slug}`)
.then(function(response) {
setPost(response.data);
})
.finally(function() {
// always executed
});
}, []);
return (
{ /* <React.Fragment> Not needed */ }
<div className="container">
<div className="row">
<div className="col-lg-8 col-md-10 mx-auto">
{ /* Update: If post is null, assume network request is
still in progress so rendering loading message instead
of single post component */ }
{ post === null ? <p>Loading</p> : <SinglePost post={post} /> }
</div>
</div>
</div>
);
};
export default PostContent;
This approach is generally the simplest and more common pattern to async requests and rendering.
There are some other approaches that you might want to consider such as this declarative approach to data fetching, or the use of Reacts suspense feature for asynchronous rendering

You need to make post's initial value an array:
import React, { useState, useEffect } from "react";
import axios from "axios";
import { apiConstants } from "../../constants";
import SinglePost from "./SinglePost";
const PostContent = props => {
const {
match: { params }
} = props;
const [post, setPost] = useState([]);
useEffect(() => {
axios
.get(apiConstants.singlePost + `${params.post_slug}`)
.then(function(response) {
setPost(response.data);
})
.finally(function() {
// always executed
});
}, []);
return (
<React.Fragment>
<div className="container">
<div className="row">
<div className="col-lg-8 col-md-10 mx-auto">
<SinglePost post={post} />
</div>
</div>
</div>
</React.Fragment>
);
};
export default PostContent;
and in single post map through the array
const SinglePost = props => {
const { post } = props;
console.log(post);
return (
<div>
{post.map((post, key) => (
<div key={key}>{post.content}</div>
))}
</div>
);
};

You can do something like
import React, { useState, useEffect } from "react";
import axios from "axios";
import { apiConstants } from "../../constants";
import SinglePost from "./SinglePost";
const PostContent = props => {
const {
match: { params }
} = props;
const [posts, setPosts] = useState([]);
useEffect(() => {
axios
.get(apiConstants.singlePost + `${params.post_slug}`)
.then(function(response) {
setPosts(response.data);
})
.finally(function() {
// always executed
});
}, []);
return (
<React.Fragment>
<div className="container">
<div className="row">
<div className="col-lg-8 col-md-10 mx-auto">
{this.state.posts.map(post => (<SinglePost post={post} key={post.id} />))
</div>
</div>
</div>
</React.Fragment>
);
};
export default PostContent;

Related

Uncaught Error: Rendered more hooks than during the previous render when trying to have a nested loop

I know same question probably asked multiple times. But I couldn't find the answer I'm looking for.
This is the code for Task:
import Navbar from './Navbar';
import "./Idea.css";
import GetData from '../restApiMethods/GetData';
import React from "react";
function Task() {
const ids = GetData("ideas/id");
return (
<div>
<Navbar />
<div className="idea-design">
<div className="container">
{
ids.map((id,index) => {
return (
<div key={index}>
{
GetData(`ideas/${id}`).map((task,index) => {
return(
<div key={index} className="row border border-secondary">
<div className="col">
<div>
<p>{task.taskDescription}</p>
</div>
</div>
</div>
)
})
}
</div>
)
})
}
</div>
</div>
</div>
)
}
export default Task
Getdata dunction:
import axios from "axios";
import {useState, useEffect} from "react";
function GetData(data) {
const [datas, setDatas] = useState([]);
useEffect(() =>{
const fetchData = () => {
axios.get(`http://localhost:8080/api/${data}`).then(res =>{
console.log(res);
setDatas(res.data);
});
};
fetchData();
}, []);
return datas;
}
export default GetData
If someone can give me some idea why I'm getting this error: Rendered more hooks than during the previous render, would be really helpful.
GetData actually is a custom hook because it's a function that calls hooks. Therefore subject to the rules of hooks.
It should be called useGetData -- I'll refer to it as that for this answer. You can't call it in a loop, as when the ids array changes length, the number of calls to useGetData will change in the parent component Task. This isn't allowed in React because hooks are supposed to be in a predictable order and never change -- it's a declarative model.
To fix this, break out a new component called Task (rename your current one to Tasks or whatever makes sense for you) and call it once in there. This doesn't break the rules of hooks as it is only within a component that the number of calls can't change between renders.
Tasks
import Navbar from "./Navbar";
import "./Idea.css";
import useGetData from "../restApiMethods/useGetData";
import React from "react";
import Task from "./Task";
function Tasks() {
const ids = useGetData("ideas/id");
return (
<div>
<Navbar />
<div className="idea-design">
<div className="container">
{ids.map((id, index) => {
return <Task id={id} key={id} />;
})}
</div>
</div>
</div>
);
}
export default Tasks;
Task
export default function Task({ id }) {
const data = useGetData(`ideas/${id}`);
return (
<div>
{data.map((task, index) => {
return (
<div key={index} className="row border border-secondary">
<div className="col">
<div>
<p>{task.taskDescription}</p>
</div>
</div>
</div>
);
})}
</div>
);
}
import axios from "axios";
import { useState, useEffect } from "react";
function useGetData(data) {
const [datas, setDatas] = useState([]);
useEffect(() => {
const fetchData = () => {
axios.get(`http://localhost:8080/api/${data}`).then((res) => {
console.log(res);
setDatas(res.data);
});
};
fetchData();
}, []);
return data;
}
export default useGetData;

React Image Gallery by categories - trying to separate the image component

I have 3 categories:
-Books
-Albums
-Movies
what I had: I had fetch and return in each component the same code (with different category, id and images), so basically decided to separate it to external component, everything works beside the url which for some reason I can't pass the variable "type" from grandparent to a grandchild and that's why images don't render. This is the code:
first child:
import React, { useState, useEffect } from "react";
export const useFetch = (address, view) => {
const [data, setData] = useState([]);
const [error, setError] = useState();
const fetchData = async (view) => {
await fetch(address)
.then((res) => res.json())
.then((json) => {
const data = json.map((elem) => {
console.log(view)
const imageUrl = `${view}${elem.id}`
const newObject = {
id: elem.id,
title: elem.title,
image: imageUrl,
};
return newObject;
});
return data;
})
.then((finalData) => {
setError(undefined);
setData(finalData);
})
.catch((e) => {
console.error("Errors: ", e);
setError(e);
});
}
useEffect(() => {
fetchData();
}, [address]);
return {data}
}
parent:
import React, { useState, useEffect } from "react";
import { useFetch } from "./useFetch"
function ImagesList({category, number, type}) {
const address = `https://jsonplaceholder.typicode.com/albums/${number}/photos`
const {data} = useFetch(address)
const view = type
return (
<div>
<h1 className="header__title"> {category}</h1>
<h1 className="header__title"> {eleven}</h1>
{/* {error && <p>Error!: {error.message}</p>} */}
<main className="container">
<div className="items-list" data-style="cards">
{data.map((item) => (
<div className="items-list__item item">
<img className="item__img" src={item.image}
/>
<div className="item__info">
<div className="caption">
<h6 className="item__name text--center">{item.title}</h6>
<div className="item__description">Statham stars as Arthur Bishop, a professional assassin who specializes in making his hits look like accidents, suicides, or the acts of petty criminals.</div>
<p className=" heart d-inline-block"> <button>"To Favorites"</button> </p>
</div>
</div>
<div>
</div>
</div>
// </div>
))}
</div>
</main>
</div>
)
}
export default ImagesList
and this is the main component (I have 3 of those total: Movies, Books and Albums, so I need to modify variables to pass them to the 2 above components)
the main one:
import React, { useState, useEffect, useContext } from "react";
import "../styles/images.css";
import "../styles/favorites.css";
import ImagesList from "./ImagesList";
const Books = (category, number) => {
return (
<ImagesList category="Books" number="2" type="https://api.lorem.space/image/book?w=150&h=220&hash=$"/>
);
}
export default Books;
Can anybody please help me? I am kind of stuck.
Thank you!

Problem when rendering image in React component

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.

How to dispatch to Reducer before React renders component?

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

rendering sorted array of objects using UseMemo

I'm trying to render the sorted array of objects using useMemo. Currently the last sorted array is rendering on the screen. But i want to use the select drop down where users can select different sort like title shown in code using useMemo. The users can sort by selecting title, author image.
I have used redux for sorting the array of objects.Could someone please help me with the best practice. Thanks.
I have added Post.js below the HomePage.js. Is my approach to it is wrong? Should i change the approach?
Any suggestions will be helpful.Could someone suggest me the best practies for it. Any suggestions on what am i doing wrong here?
HomePage.js
import React, { useState, useEffect, useMemo } from "react";
import Post from "../../Components/Post/Post";
import "./HomePage.css";
import axios from "axios";
const HomePage = () => {
const [posts, setPosts] = useState("");
let config = { Authorization: "................" };
const url = ".........................";
useEffect(() => {
AllPosts();
}, []);
const AllPosts = () => {
axios
.get(`${url}`, { headers: config })
.then((response) => {
const allPosts = response.data.articles;
console.log(response);
})
.catch((error) => console.error(`Error: ${error}`));
};
const newPostsByTitle = useMemo(() => {
allPosts.sort((a, b) => a.title.localeCompare(b.title)), [posts];
});
return (
<div className="home">
<div className="select">
<select
name="slct"
id="slct"
onChange={(e) => newPostsByTitle(e.target.value)}
></select>
</div>
<Post className="Posts" posts={posts} key={posts.title} />
</div>
);
};
export default HomePage;
Post.js
import React from "react";
import "./Post.css";
import { Fragment } from "react";
const Post = (props) => {
const displayPosts = (props) => {
const { posts } = props;
if (posts.length > 0) {
return posts.map((post) => {
return (
<Fragment>
<div className="Post" key={post.title}>
<img
src={post.urlToImage}
alt="covid"
width="100%"
className="img"
/>
<h5 className="title"> {post.title}</h5>
<p className="author"> {post.author}</p>
<p className="description"> {post.description}</p>
</div>
</Fragment>
);
});
}
};
return <div className="Posts">{displayPosts(props)}</div>;
};
export default Post;
There are few issues with the useMemo function.
There is no allPosts variable that will be available for that
function
There is no return inside useMemo
The dependency array syntax is wrong.
It should be something like the following.
const newPostsByTitle = useMemo(() => {
return [...posts].sort((a, b) => a.title.localeCompare(b.title));
}, [posts]);

Resources