Move element from child component to its parent - javascript

I want to design an ApiFetcher in order to prevent duplicate code.
How to I pass a custom child component to my ApiFetcher so that it renders the content I specify in the custom component instead of a hard coded element in the return statement?
I want to keep the logic of the ApiFetcher and the CompanyProfile components out of their parent components.
import React from "react";
import ReactDOM from "react-dom";
import { useState, useEffect } from "react";
function ApiFetcher(props) {
const apiUrl =
"https://financialmodelingprep.com/api/v3/profile/AAPL?apikey=demo";
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
(error) => {
setIsLoaded(true);
setError(error);
}
);
}, []);
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
// TODO: move the following <div> out of this component and render children
return (
<div>
{items.map((item) => (
<li key={item.symbol}>
{item.companyName} {item.price}
</li>
))}
</div>
);
}
}
function CompanyProfile() {
return (
<div>
<ApiFetcher>
{/*
TODO: move the following into this component:
<div>
{items.map((item) => (
<li key={item.symbol}>
{item.companyName} {item.price}
</li>
))}
</div>
*/}
</ApiFetcher>
</div>
);
}
ReactDOM.render(<CompanyProfile />, document.getElementById("root"));

You can maybe use props.children in order to move rendering logic to Parent component. Here's how I will strucutre the component:
function ApiFetcher({url}) {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
(error) => {
setIsLoaded(true);
setError(error);
}
);
}, []);
if (error) {
return <div>Error: {error.message}</div>;
}
if (!isLoaded) {
return <div>Loading...</div>;
}
return props.children({response: items});
}
Usage
<ApiFetcher>
{({response}) => // Rendering logic}
</ApiFetcher>

Related

How can i add React loading spinner when item is fetching from database?

Here is, my code. i want to add a spinner when data is loading then after finish loading display the data.
{/* product section */}
<section className="container mx-auto my-5 row">
<h6 className="text-center">WANT TO SEE</h6>
<h2 className="text-center mb-5">Our Services</h2>
{
Products.slice(0,6)?.map((product) => {
return <Product key={product._id} product={product}></Product>;
})
}
</section>
And This is the custom hook
import { useEffect, useState } from "react";
const LoadProducts = () => {
const [Products, setProducts] = useState([]);
useEffect(()=>{
fetch('http://localhost:8080/products')
.then(res=>res.json())
.then(data=>setProducts(data))
},[]);
return [Products, setProducts]
}
export default LoadProducts;
Try this:
import { useEffect, useState } from "react";
const LoadProducts = () => {
const [Products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(()=>{
setLoading(true);
fetch('http://localhost:8080/products')
.then(res=>res.json())
.then(data=>setProducts(data))
.finally(()=>setLoading(false))
},[]);
return [Products, setProducts, loading]
}
export default LoadProducts;
Note the custom hook now returns [Products, setProducts, loading].
Now you can add some conditional code to your JSX that shows a loading spinner while loading is true
{/* product section */}
<section className="container mx-auto my-5 row">
<h6 className="text-center">WANT TO SEE</h6>
<h2 className="text-center mb-5">Our Services</h2>
{loading ? (
<MyLoadingSpinner/>
) : (
Products.slice(0,6)?.map((product) => {
return <Product key={product._id} product={product}></Product>;
})
)}
</section>
I would do something like this but you can customize it to your needs. The trick is really to create a loading state which initially will be false. When you start calling your api, you will set your loading to true. Once you either get a response or fail you set it to false.
import { useEffect, useState } from "react";
export default function App() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((json) => {
console.log(json);
setPosts(json);
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
}, []);
if (loading) {
return <div>I am loading</div>;
}
return (
<div>
{posts.map((post) => (
<p>{post.title}</p>
))}
</div>
);
}

Exporting Async API call to another file

Im'trying to seperate api call from return/rendering but when importing teams (which contains data) shows as undefined. Is it because of the async await function?
How can I export the data and map it in the return on Teams.js?
Is there a better way to seperate the Api call from the return/rendering component?
FormTeams.js
import { useState, useEffect } from 'react';
import { Spinner } from "react-bootstrap";
import 'bootstrap/dist/css/bootstrap.min.css';
const FormTeams = () => {
const BASE_URL = process.env.REACT_APP_URL
const [isLoading, setIsLoading] = useState(true);
const [teams, setTeams] = useState([]);
useEffect(() => {
getTeams();
}, []);
const getTeams = async () => {
try {
const response = await fetch(`${BASE_URL}/teams`)
return response.json()
.then(data => {
setTeams(data)
setIsLoading(false)
})
} catch (error) {
console.log(error)
}
}
if (isLoading) {
return (<Spinner animation="border" variant="primary" />)
}
const deleteTeam = async (id) => {
try {
await fetch(`${BASE_URL}/teams/${id}`, {
method: "DELETE",
}).then(response => {
setTeams(teams.filter(team => team.id !== id))
return response.json()
})
} catch (error) {
console.log(error)
}
}
return {teams, deleteTeam}
}
export default FormTeams
Teams.js
import { Link, } from "react-router-dom";
import { Button, ButtonGroup } from "react-bootstrap";
import FormTeams from './FormTeams'
const Teams = () => {
const {teams, deleteTeam} =FormTeams()
return (
<div>
<h2 className='centered'>Clubs</h2>
<div><Link to="/Teams/add" className="link">Add New</Link></div>
<table className='teams'>
<thead>
<tr >
<th>№</th>
<th>Team</th>
<th>Actions</th>
</tr>
</thead>
<tbody >
{teams.map((team, index) => (
<tr key={team.id}>
<td>{index + 1}.</td>
<td>{team.team_name}</td>
<td>
<ButtonGroup>
<Link to={`/Teams/${team.id}`} className='link'>View</Link>
<Link to={`/Teams/edit/${team.id}`} className='edit'>Edit</Link>
<Button variant="danger" onClick={() => deleteTeam(team.id)}>Delete</Button>
</ButtonGroup>
</td>
</tr>
))}
</tbody>
</table>
<Link to={'/'} className='link'>Back To Home Page</Link>
</div>
)
}
export default Teams
You're kind of close. There are a couple things you need to change:
As a matter of convention, components (anything which renders content) should always have a PascalCase name (TeamsTable), and hooks should have a name which begins with the word use and is camelCased: useFormData.
Another matter of convention: write your effect code all in one block:
useEffect(async () => {
try { ... }
catch(error) { ... }
}, []);
Make sure your effect declares the right dependencies. In your case, you are referencing the BASE_URL variable. And even though you know the value will never change, you still need to list it as a dependency for your effect:
useEffect(..., [BASE_URL]);
Hooks should not render anything. So move your loader into the TeamsTable component.
You also need to make sure that you set isLoading back to false if there is an error. I highly recommend keeping "error" state too and updating it inside all catch blocks:
const [error, setError] = useState(null);
...
try { ... }
catch(error) {
setError(error);
setIsLoading(false);
}
...
Wrapping it all together (only showing relevant changes)
You should have a new useFormTeams hook instead of the FormTeams function you have now, and the useEffect call should be updated per my suggestions. You should also return the isLoading state:
const useFormTeams = () => {
const [error, setError] = useState(null);
...
useEffect(async () => {
try {
// do async stuff
setError(null);
setIsLoading(false);
}
catch(error) {
setError(error);
setIsLoading(false);
}
}, [BASE_URL]
...
return { teams, error, deleteTeam, isLoading };
};
And you will use your new hook as follows:
const TeamsTable = () => {
const { teams, error, deleteTeams, isLoading } = useFormTeams();
...
if (isLoading) {
return <Spinner ... />
}
if (error) {
return <div>There was an error: {error}<div>
}
...
};
Yes. This is where useHooks to the rescue!
First, separate all the logics into a hook
const useTeams = () => {
const BASE_URL = process.env.REACT_APP_URL
const [isLoading, setIsLoading] = useState(true);
const [teams, setTeams] = useState([]);
const getTeams = async () => {
try {
const response = await fetch(`${BASE_URL}/teams`)
return response.json()
.then(data => {
setTeams(data)
setIsLoading(false)
})
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getTeams();
}, []);
const deleteTeam = (id) => {
fetch(`${BASE_URL}/teams/${id}`, {
method: "DELETE",
}).then(response => {
setTeams(teams => teams.filter(team => team.id !== id))
}).catch(error => {
console.log(error)
}
}
return { teams, deleteTeam, isLoading }
}
Then use the returned values as props for the return elements
const Teams = () => {
const { teams, deleteTeam, isLoading } = useTeams()
if (isLoading) {
return <Spinner animation="border" variant="primary" />
}
return (
<div>
<h2 className='centered'>Clubs</h2>
<div><Link to="/Teams/add" className="link">Add New</Link></div>
<table className='teams'>
<thead>
<tr >
<th>№</th>
<th>Team</th>
<th>Actions</th>
</tr>
</thead>
<tbody >
{teams.map((team, index) => (
<tr key={team.id}>
<td>{index + 1}.</td>
<td>{team.team_name}</td>
<td>
<ButtonGroup>
<Link to={`/Teams/${team.id}`} className='link'>View</Link>
<Link to={`/Teams/edit/${team.id}`} className='edit'>Edit</Link>
<Button variant="danger" onClick={() => deleteTeam(team.id)}>Delete</Button>
</ButtonGroup>
</td>
</tr>
))}
</tbody>
</table>
<Link to="/" className="link">Back To Home Page</Link>
</div>
)
}
IMO, The core idea of react hooks is just to separate logics from rendering. And this is the perfect use case for your problem. The hooks provide as a blackbox of logics that may composed of multiple other hooks and can mainly used for providing data or do some behavior.

React-router-dom, useNavigate(-1) problem

I am working on countries project.
I get information about this when the border buttons are clicked. But when I click the Back button, the previous country data does not appear. How can I fix this? Please help me!
Here is my Country Component
import React, { useEffect, useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import Loading from './Loading';
function Country() {
const { countryCode } = useParams();
const navigate = useNavigate();
const [country, setCountry] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { getSingleCountryData(countryCode); }, []);
const getSingleCountryData = async (countryCode) => {
setLoading(true);
try {
const country = JSON.parse(localStorage.getItem("countries")).filter(c => c.cca3 === countryCode);
setCountry(country[0]);
setLoading(false);
} catch (err) {
console.log(err);
}
}
return loading ? <Loading />
: (
<div className='country container'>
<button className='btn backBtn' onClick={() => navigate(-1)}> Back </button>
// some code
<div className="country__borders">
<h4>Border Countries:</h4>
{country.borders && country.borders.map((border, index) => {
return <Link to={`/countries/${border}`} onClick={() => getSingleCountryData(border)} key={index} className='btn'>{border}</Link>
})}
</div>
</div>
);
}
export default Country;
The useEffect hook is missing the getSingleCountryData and countryCode as dependencies. You'll want to memoize the getSingleCountryData callback so it's provided to the useEffect hook as a stable callback reference.
const getSingleCountryData = useCallback(async (countryCode) => {
setLoading(true);
try {
const country = (JSON.parse(localStorage.getItem("countries")) || [])
.filter(c => c.cca3 === countryCode);
setCountry(country[0]);
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
getSingleCountryData(countryCode);
}, [countryCode, getSingleCountryData]);
Since the country data is fetched and loaded when the countryCode route param updates there'll no longer be a need to trigger this fetching when the link is clicked.
{country.borders && country.borders.map((border, index) => (
<Link
key={index}
to={`/countries/${border}`}
className='btn'
>
{border}
</Link>
))}
And since the data is not longer fetched via a click handler and only referenced in the useEffect, getSingleCountryData can be moved into the useEffect hook and be removed entirely as a dependency.
const getSingleCountryData = useCallback(, []);
useEffect(() => {
const getSingleCountryData = async (countryCode) => {
setLoading(true);
try {
const country = (JSON.parse(localStorage.getItem("countries")) || [])
.filter(c => c.cca3 === countryCode);
setCountry(country[0]);
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
}
getSingleCountryData(countryCode);
}, [countryCode]);

Get from api in useEffect and render components accordingly

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>
)
}

Duplicate keys in React, can't solve the issue. Encountered two children with the same key

Error:
Warning: Encountered two children with the same key, 5e0611d77833da1668feade1. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
Here on this picture https://prnt.sc/qgfymk I have created 2 blogs. Delete button is working fine. I'm sending via axios to HTTP delete request using mongoose and MongoDB as my database.
But when I start to click on like button check what happens. https://prnt.sc/qgg32o
It removes my other blog post and copies one with the same name and id. The issue here is that I have different IDs but somehow when I press LIKE button it gives me another ID.
I'll give you code for both PUT request in backend and frontend for incrementLikes, I really don't know what is going on.
controllers/blogs.js (backend)
blogsRouter.put('/:id', async (request, response, next) => {
const body = request.body
const blogs = {
title:body.title,
author: body.author,
url:body.url,
likes: body.likes
}
try {
const updatedBlog = await Blog.findOneAndUpdate(request.params.id, blogs, {
new: true
})
response.json(updatedBlog.toJSON())
} catch (exception) {
next(exception)
}
})
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import Blog from './components/Blog';
import LoginForm from './components/LoginForm'
import BlogForm from './components/BlogForm'
import Notification from './components/Notification'
import loginService from './services/login';
import blogService from './services/blogs';
const App = () => {
const [blogs, setBlogs] = useState([])
const [user, setUser] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState(null)
// states for blog creation
const [title, setTitle] = useState('')
const [author, setAuthor] = useState('')
const [url, setUrl] = useState('')
useEffect(() => {
console.log('effect')
blogService
.getAll()
.then(response => {
console.log('promise fulfiled')
setBlogs(response.data)
})
.catch(error => {
console.log('response', error.response)
console.log('error')
})
}, [])
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
}, [])
//put request
***const incrementLike = id => {
const blog = blogs.find(b => b.id === id)
console.log('blog id', blog)
const voteLike = {...blog, likes: blog.likes + 1}
blogService
.update(id, voteLike)
.then(returnedBlog => {
setBlogs(blogs.map(blog => blog.id !== id ? blog : returnedBlog))
})
.catch(error => {
setErrorMessage(
`Blog was already removed from server`
)
setTimeout(() => {
setErrorMessage(null)
}, 5000)
})
}***
//login
const handleLogin = async (e) => {
e.preventDefault()
try {
const user = await loginService.login({username, password})
window.localStorage.setItem('loggedBlogUser', JSON.stringify(user))
setUser(user)
setUsername('')
setPassword('')
console.log('success')
} catch (exception) {
setErrorMessage('wrong credentials')
setTimeout(() => {
setErrorMessage(null)
}, 5000)
console.log('baaad')
}
}
const deleteBlogId = (id) => {
console.log('deleted blog')
blogService
.del(id)
.then(response => {
setBlogs(blogs.filter(blog => blog.id !== id))
})
.catch(error => {
console.log(error.response);
})
}
const handleCreateBlog = async (e) => {
e.preventDefault()
const newBlogs = {
title: title,
author: author,
url: url,
date: new Date()
}
blogService
.create(newBlogs)
.then(returnedBlog => {
setBlogs(blogs.concat(returnedBlog))
setTitle('')
setAuthor('')
setUrl('')
setErrorMessage(`${author} created new blog with name ${title}`)
setTimeout(() => {
setErrorMessage(null)
}, 5000)
})
}
const loginForm = () => {
return (
<div>
<Notification message={errorMessage}/>
<div>
<LoginForm
username={username}
password={password}
handleUsernameChange={({target}) => setUsername(target.value)}
handlePasswordChange={({target}) => setPassword(target.value)}
handleSubmit={handleLogin}
/>
</div>
</div>
)
}
const handleTitleChange = (event) => {
console.log(event.target.value)
setTitle(event.target.value)
}
const blogForm = () => {
return (
<div>
<BlogForm
title={title}
author={author}
url={url}
handleTitleChange={handleTitleChange}
handleAuthorChange={({target}) => setAuthor(target.value)}
handleUrlChange={({target}) => setUrl(target.value)}
onSubmit={handleCreateBlog}
/>
</div>
)
}
const handleLogout = async () => {
window.localStorage.clear()
setUser(null)
}
const logout = () => {
return (
<div><button type="reset" onClick={handleLogout}>Logout</button></div>
)}
const blogList = () => {
return (
<div>
<h2>Blogs</h2>
<p>{user.name} logged in</p>
{logout()}
{blogs.map(blog =>
<Blog
key={blog.id}
deleteBlog={() => deleteBlogId(blog.id)}
blog={blog}
increment={() => incrementLike(blog.id)} />
)}
</div>
)
}
return (
<div className="App">
{user === null ?
loginForm() :
<div>
<Notification message={errorMessage}/>
{blogForm()}
{blogList()}
</div>
}
</div>
);
}
export default App;
Check the incrementLikes function. I think there is some kind of issue. Button for likies are in component called Blog.js
Blog.js
import React from 'react';
const Blog = ({blog, increment, deleteBlog}) => (
<div>
<button onClick={deleteBlog}>Delete</button>
{blog.title}
{blog.author}
{blog.likes}
<button onClick={increment}>Like</button>
</div>
)
export default Blog
While there shouldn't be 2 blogs with the same ID you can fix the issue at hand by replacing the key from blog.id to the index of the post like this.
<div>
<h2>Blogs</h2>
<p>{user.name} logged in</p>
{logout()}
//change
{blogs.map((blog,index) =>
<Blog
//change
key={index}
deleteBlog={() => deleteBlogId(blog.id)}
blog={blog}
increment={() => incrementLike(blog.id)} />
)}
</div>
I added //change to the lines I changed.
You can just use something like uuid for this which will generate a unique ID.
import uuid from "uuid";
<>
<h2>Blogs</h2>
<p>{user.name} logged in</p>
{logout()}
{blogs.map((blog,index) =>
<Blog
key={uuid.v4()}
deleteBlog={() => deleteBlogId(blog.id)}
blog={blog}
increment={() => incrementLike(blog.id)} />
)}
</>

Categories