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

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]);

Related

How to call api only on mount and click of Fetch Next User button

I have the below code
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const [nextPageNumber, setNextPageNumber] = useState(1);
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageNumber}`).then(response => {
const updatedUsers = [...users, ...response.data.results];
setUsers(updatedUsers);
setNextPageNumber(response.data.info.page + 1);
}).catch(error => {
console.log(error)
})
}, [nextPageNumber, users])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const fetchNextUser = () => {
fetchUsers();
}
if(users.length === 0){
return <div>No Users available</div>
}
return(
<div>
<button onClick={fetchNextUser}>Fetch Next user</button>
{users.map(user => (
<div key={user.id}>
<div>Name: {user.name.title} {user.name.first} {user.name.last}</div>
<div>
<img src={user.picture.large} alt="Not available"/>
</div>
</div>
))}
</div>
)
}
export default Users;
This is calling the api repeatadlly. I have used the fetchUsers dependency in useEffect and useCallback hook due to eslint errors. I just want to call the api on first mount and on click of Fetch Next user button without any eslint error.
Is there any way we can achieve that?
Have a try with the below changes it will not give you the eslint error messages.
import React, {useState, useEffect, useRef, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const nextPageRef = useRef(1)
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageRef.current}`).then(response => {
const updatedUsers = [...response.data.results];
setUsers(prevUsers => [...prevUsers, ...updatedUsers]);
nextPageRef.current = response.data.info.page + 1
}).catch(error => {
console.log(error)
})
}, [])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
You could simply use
useEffect(() => {
fetchUsers();
}, []);
This will call the fetchUsers function only once.
And when button is pressed, again call the fetchUsers() function

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

How to wait until context value is set?

I'm trying to render a header.
First, in InnerList.js, I make an API call, and with the data from the API call, I set a list in context.
Second, in Context.js, I take the list and set it to a specific data.
Then, in InnerListHeader.js, I use the specific data to render within the header.
Problem: I currently get a TypeError undefined because the context is not set before rendering. Is there a way to wait via async or something else for the data to set before loading?
My code block is below. I've been looking through a lot of questions on StackOverflow and blogs but to no avail. Thank you!
InnerList.js
componentDidMount() {
const { dtc_id } = this.props.match.params;
const {
setSpecificDtcCommentList,
} = this.context;
MechApiService.getSpecificDtcCommentList(dtc_id)
.then(res =>
setSpecificDtcCommentList(res)
)
}
renderSpecificDtcCommentListHeader() {
const { specificDtc = [] } = this.context;
return (
<InnerDtcCommentListItemHeader key={specificDtc.id} specificDtc={specificDtc} />
)
}
Context.js
setSpecificDtcCommentList = (specificDtcCommentList) => {
this.setState({ specificDtcCommentList })
this.setSpecificDtc(specificDtcCommentList)
}
setSpecificDtc = (specificDtcCommentList) => {
this.setState({ specificDtc: specificDtcCommentList[0] })
}
InnerListHeader.js
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
In general, you should always consider that a variable can reach the rendering stage without a proper value (e.g. unset). It is up to you prevent a crash on that.
For instance, you could rewrite you snippet as follows:
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{Boolean(specificDtc.dtc_id) && specificDtc.dtc_id.dtc}
</div>
</div>
);
}
When you make an api call you can set a loader while the data is being fetched from the api and once it is there you show the component that will render that data.
In your example you can add a new state that will pass the api call status to the children like that
render() {
const { specificDtc, fetchingData } = this.props;
if (fetchingData){
return <p>Loading</p>
}else{
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
}
``
in my case, i am calling external api to firebase which lead to that context pass undefined for some values like user. so i have used loading set to wait untile the api request is finished and then return the provider
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
updateProfile
} from 'firebase/auth';
import { auth } from '../firebase';
import { useNavigate } from 'react-router';
import { create_user_db, get_user_db } from 'api/UserAPI';
import { CircularProgress, LinearProgress } from '#mui/material';
import Loader from 'ui-component/Loader';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState();
const [user_db, setUserDB] = useState();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const createUser = async (email, password) => {
const user = await createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const googleSignIn = async () => {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const logout = () => {
setUser();
return signOut(auth).then(() => {
window.location = '/login';
});
};
const updateUserProfile = async (obj) => {
await updateProfile(auth.currentUser, obj);
return updateUser(obj);
};
const updateUser = async (user) => {
return setUser((prevState) => {
return {
...prevState,
...user
};
});
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
setLoading(true);
if (currentUser) {
const user_db = await get_user_db({ access_token: currentUser.accessToken });
setUserDB(user_db);
setUser(currentUser);
setIsAuthenticated(true);
}
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
if (loading) return <Loader />;
return (
<UserContext.Provider value={{ createUser, user, user_db, isAuthenticated, logout, signIn, googleSignIn, updateUserProfile }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};

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

Component router after timeout with state

I currently have a component that does a history.push('/') after a few seconds. But I am getting warnings
index.js:1375 Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
and also
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I am fairly new to React, do I need do some sort of clean up?
Here is my component.
import React, {useState} from 'react'
import {UsePostOrPutFetch} from "../hooks/postHook";
import "./styles/ConfirmationChange.scss";
import moment from 'moment';
export default function ConfirmatonChange(props) {
const [shouldFetch, setShouldFetch] = useState(false);
const [data,loading,isError, errorMessage] = UsePostOrPutFetch("/send-email/", props.data.location.state.value,"POST", shouldFetch, setShouldFetch);
const [countDown, setCountDown] = useState(5)
let spinner = (
<strong className="c-spinner" role="progressbar">
Loading…
</strong>
);
const changeView = () =>
{
if (countDown < 0) {
props.data.history.push('/')
} else {
setTimeout(() => {
setCountDown(countDown - 1)
}
, 1000)
}
}
return (
<div>
<div className="o-container">
<article className="c-tile">
<div className="c-tile__content">
<div className="c-tile__body u-padding-all">
<button className = "c-btn c-btn--primary u-margin-right" onClick={props.data.history.goBack}>Edit</button>
<button className = "c-btn c-btn--secondary u-margin-right" disabled={loading} onClick={(e) => { setShouldFetch(true)}}>Send</button>
{!loading && data === 200 && !isError ? (
<div className="ConfirmationChange-success-send">
<hr hidden={!data === 200} className="c-divider" />
Email Sent succesfully
<p>You will be redirected shortly....{countDown}</p>
{changeView()}
</div>
) : (loading && !isError ? spinner :
<div className="ConfirmationChange-error-send">
<hr hidden={!isError} className="c-divider" />
{errorMessage}
</div>
)}
</div>
</div>
</article>
</div>
</div>
)
}
And here is what my data fetch component look like
import { useState, useEffect } from "react";
import { adalApiFetch } from "../config/adal-config";
const UsePostOrPutFetch = (url, sendData, methodType, shouldFetch, setShouldSend) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setError] = useState("");
useEffect(() => {
const ac = new AbortController();
if (shouldFetch) {
const postOrPutData = async () => {
try {
const response = await adalApiFetch(fetch, url,
{
signal: ac.signal,
method: methodType,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(sendData)
});
const json = await response.json();
setData(await json);
setLoading(true);
} catch (err) {
setIsError(true);
setError(err.message);
} finally {
setShouldSend(false)
setLoading(false);
}
};
postOrPutData();
}
return () => { ac.abort(); };
}, [shouldFetch, sendData, url, methodType, setShouldSend]);
return [data, loading, isError, errorMessage];
};
export {UsePostOrPutFetch}
Any help would be greatly appreciated.
Check React Hooks - Check If A Component Is Mounted
The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes.
You'll need a componentIsMounted variable and useEffect and useRef hooks :
const componentIsMounted = useRef(true);
useEffect(() => {
return () => {
componentIsMounted.current = false;
};
}, []);
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
setTimeout(() => {
if (componentIsMounted.current) { // only update the state if the component is mounted
setCountDown(countDown - 1);
}
}, 1000);
}
};
You should do the same for data fetch component
Yup, you have a timeOut that could potentially fire after the component has unmounted.
You need to add a useEffect that clears the timer onUnmount as follows
const timerRef = useRef();
useEffect(() => () => clearTimeout(timerRef.current), [])
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
timerRef.current = setTimeout(() => {
setCountDown(countDown - 1);
}, 1000);
}
};

Categories