Changing props state using api causes infinite loop - reactjs

I don't know why, changing the props state inside useEffect causes infinite loop of errors. I used them first locally declaring within the function without using props which was running ok.
EDIT:
Home.js
import Axios from "axios";
import React, { useEffect, useState } from "react";
function Home(props) {
// const [details, setDetails] = useState({});
// const [login, setLogin] = useState(false);
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
props.setLogin(true);
props.setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return props.login ? (
<div>
<div>Welcome {props.setUserdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
}
export default Home;
App.js
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setLogin={setLogin}
login={login}
setUserdetails={setUserdetails}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
Here I initialized the states directly in App.js so I don't have to declare it on every page for the route renders. I just passed them as props to every component.

I suggest to create a componente Home with the post and two sub-component inside:
const Home = () => {
const [userDetails, setUserDetails] = useState({});
const [login, setLogin] = useState(false);
useEffect(() => {
// api call
}, []);
return (
<>
<Welcome login={login} details={userDetails} />
<Bars login={login} details={userDetails} />
</>
);
};
where Welcome is the following:
const Welcome = ({ userdetails, login }) => (
<>
login ? (
<div>
<div>Welcome {userdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
</>
);
A better solution is to use only one state variable:
const [userDetails, setUserDetails] = useState(null);
and test if userDetails is null as you test login is true.
An alternative if you have to maintain the call as you write before, you can use two state as the follow:
function App() {
const [userdetails, setUserdetails] = useState(null);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setUserdetails={setUserdetails}
/>
<Bars login={!!userdetails} />
</>
}
/>
<Routes>
<Router>
);
and on Home component use a local state:
const Home = ({setUserdetails}) => {
const [userDetailsLocal, setUserDetailsLocal] = useState(null);
useEffect(() => {
// api call
// ... on response received:
setUserdetails(res.data[0]);
setUserDetailsLocal(res.data[0]);
// ...
}, []);
userDetailsLocal ? (
<div>
<div>Welcome {userDetailsLocal.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
};

I advise to follow Max arquitecture for your solution. the problem lies in the Router behavior. React Router is not part of React core, so you must use it outside your react logic.
from documentation of React Router:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render.
https://v5.reactrouter.com/web/api/Route/component
Edit:
ok, you make me write it. A solution could be like:
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
setLogin(true);
setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
login={login}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);

Related

Link to single post in react

I fetch and map posts from https://jsonplaceholder.typicode.com/posts
Already I have posts on my website but how can I go to single post and have unique information I mean body and title by id of post?
Any idea how can i do this?
function App() {
return (
<Layout>
<Switch>
<Route path='/' exact={true}>
<Posts />
</Route>
<Route path='/favorite-posts'>
<FavoritePosts />
</Route>
<Route path='/single-post:id'>
<SinglePost />
</Route>
<Route>
<Page404 path='*' />
</Route>
</Switch>
</Layout>
)
}
Looks like pages are working but how can I pass title and body from fetch into SinglePage?
const Posts = () => {
const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetchData()
}, [])
// useEffect(() => {
// fetchImg()
// }, [])
const fetchData = async () => {
let response = await fetch('https://jsonplaceholder.typicode.com/posts')
await response
.json()
.then((finish) => {
setIsLoading(false)
setData(finish) //.splice(0, 5)
console.log(finish)
})
.catch((error) => {
console.error('Houston, we have a problem.. with fetch')
})
}
//loading animation
if (isLoading) {
return <Loading />
}
return (
<>
<BlogPosts datas={data} />
</>
)
}
export default Posts
const Post = ({ title, body, random, id }) => {
const favoritesCtx = useContext(FavoritesContext)
const itemIsFavorite = favoritesCtx.itemIsFavorite(id)
function toggleFavoriteStatusHandler() {
if (itemIsFavorite) {
favoritesCtx.removeFavorite(id)
} else {
favoritesCtx.addFavorite({
id: id,
title: title,
body: body,
})
}
}
return (
<article className={styles.box}>
<Link to={`/single-post:${id}`}>
<img
className={styles.box__image}
// src={`https://rickandmortyapi.com/api/character/avatar/${random}.jpeg`}
src={`https://rickandmortyapi.com/api/character/avatar/19.jpeg`}
alt='test'
/>
</Link>
<button
className={itemIsFavorite ? styles.box__btn_two : styles.box__btn}
onClick={toggleFavoriteStatusHandler}
>
{itemIsFavorite ? 'Remove from Favorites' : 'Add to Favorites'}
</button>
<h4 className={styles.box__title}>{title}</h4>
</article>
)
}
const SinglePost = ({ title, body, id }) => {
return (
<section>
<p>{id}</p>
<h1>{title}</h1>
<h2>{body}</h2>
<p>hey</p>
</section>
)
}
export default SinglePost
You need to make few adjustments for that. If you want to have a dedicated page for each post, then SinglePost will act as a page.
You'll have to add a route which accepts the post id as a param and on basis of that fetches the data for that particular post and renders this component. Something like this:
<Switch>
//...
<Route path='/post/:id'>
<SinglePost />
</Route>
//...
</Switch>
Now in SinglePost file, get the id from route param, and make an api call.
import {useParams, useState, useEffect} from 'react'
const SinglePost = (props) => {
//this is where we will store data after getting from api
const [post, setPost] = useState();
// get id from route param using this hook
const id = useParams().id;
// then in useEffect call the api to fetch data for single post
useEffect(()=>{
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`).then(res => {
setPost(res.data);
})
},[])
return (
<section>
<h1>{post?.title}</h1>
<h2>{post?.body}</h2>
</section>
)
}
export default SinglePost;
Hope you get the picture.
Add this to your route
<Route path='/post:id'>
<Post />
</Route>
And add Link to your Post component to redirect
<Link to={"/post"+{item._id}/>
Ok I did it !
import { useLocation } from 'react-router'
import { useEffect, useState } from 'react'
import styles from './SinglePost.module.css'
import Loading from '../ui/Loading'
import Comments from './Comments'
const SinglePost = () => {
const location = useLocation()
const path = location.pathname.split('/')[2]
const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [comm, setComm] = useState([])
useEffect(() => {
fetchData()
}, [])
useEffect(() => {
fetchComment()
}, [])
const fetchData = async () => {
let response = await fetch(
'https://jsonplaceholder.typicode.com/posts/' + path
)
await response.json().then((finish) => {
setIsLoading(false)
setData(finish)
console.log(finish)
})
}
const fetchComment = async () => {
let response = await fetch('https://jsonplaceholder.typicode.com/comments/')
await response.json().then((finish) => {
setIsLoading(false)
setComm(finish.splice(0, 7)) //random
console.log(finish)
})
}
if (isLoading) {
return <Loading />
}
return (
<section className={styles.single}>
<div className={styles.single__text}>
<h1>{data.title}</h1>
<p>{data.body}</p>
<h4>Comments:</h4>
</div>
<div className={styles.comments}>
{comm.map((comme) => {
return (
<Comments
key={comme.id}
name={comme.name}
email={comme.email}
body={comme.body}
/>
)
})}
</div>
</section>
)
}
export default SinglePost

Redux Action only dispatched on refresh and not after redirect

I used a history.push("/") to redirect to my homepage from another page and the homepage has a ProjectList component that dispatches an action:
App.js
function App(){
return(
<Router>
<NavBar></NavBar>
<Switch>
<Route
path="/createProject"
component={withAuthentication(CreateProjectPage)}
></Route>
<Route
exact
path="/"
component={withAuthentication(HomePage)}
></Route>
</Switch>
</Router>
);
}
export default App
CreateProjectPage
function CreateProject(){
const dispatch = useDispatch();
const history = useHistory();
const submitHandler = (e) => {
...
dispatch(createNewProject({...}));
history.push("/");
}
return (
<div>
...
<Button
onClick={submitHandler}
variant="contained"
color="primary"
>
Submit
</Button>
</div>
)
}
HomePage
export default function HomePage(props){
return (
<div>
<ProjectList level="one"/>
...
</div>
);
}
ProjectList Component:
export default function ProjectList({level}){
console.log(level); //onRedirect, this gets called
const dispatch = useDispatch();
useEffect(() => {
console.log("dispatch returnAllProjects called") //onRedirect, this does not get called
dispatch(returnAllProjects());
}, [dispatch]);
...
return ...
}
only on refresh this returnAllProjects is called.
This results in getting undefined objects.
Any help would be greatly appreciated!
You should add location as dependency if you want the effect to get called after redirect:
import { useLocation } from 'react-router-dom';
export default function ProjectList({level}){
const location = useLocation(); // <--- listen to location
const dispatch = useDispatch();
useEffect(() => {
dispatch(returnAllProjects());
}, [dispatch, location]); //<--- location is added as dependency
...
return ...
}

How to use the useHook in the component rather than passing the value returned from it as a prop to the components using react and typescript?

i want to use useHook within the component itself rather than passing it as a prop to each component using react and typescript.
what i am trying to do?
I have a useHook named useRefresh which returns isLoading state. This isLoading state is used to display a loading indicator in each of the pages.
so i have three pages and whenever this isLoading is true should display a loading indicator in these pages.
below is my code,
function App(){
const user = useGetUser();
return (
<Router>
<Switch>
<Route
path="/"
render={props: any => (
user ? (<Main {...props} />) : (
<LoginPage/>
);
)}
</Route>
</Switch>
</Router>
);
}
export function useLoad() {
const { refetch: refetchItems } = useGetItems();
const { refetch: refetchOwnedItems } = useListOwnedItems();
return async function() {
await refreshCompany();
refetchItems();
refetchOwnedItems();
};
}
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id);
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
function SecondComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
Now i want to pass isLoading state to each of the components in Main component....so i have passed it like below,
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp isLoading={isLoading} {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp isLoading={isLoading} {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
This works. but doesnt seem like a right approach to me.. i dont want to pass this isLoading state as a prop to each of these components. there are more than 10 of them.
is there someway that i can do it other way than this. could someone help me with this. thanks.
The most common solution is to create a context that wraps the entire tree of components. This context holds the state that your hook pulls in
////LoadingContext.tsx
const LoadingContext = createContext();
const LoadingContextProvider = () => {
const [isLoading, setIsLoading] = useState(false);
return (
<LoadingContextProvider.Provider
value={{
isLoading,
setIsLoading
}}
/>
)
}
export const useLoading = () => useContext(LoadingContext);
You need to wrap the context around anything that will be calling useLoading:
import { LoadingContextProvider } from './LoadingContext' //or wherever this is relative to Main.tsx
<LoadingContextProvider>
<Router>
...(router stuff)
</Router>
</LoadingContextProvider>
Now you can call useLoading in your lower-level components.
//in another file defining a lower-level component:
import { useLoading } from '../../LoadingContext' //or wherever the context stuff is relative to this component definition
const FirstComp = () =>
const [isLoading, setIsLoading] = useLoading();
const handleClick = () => {
setIsLoading(true);
callMyApi().then(() => setIsLoading(false));
}
if(isLoading){
return <LoadingGif />
}
else{
return <div onClick={handleClick}>Click me!</div>
}
)}
What you would like to accomplish here is called global state. There are many ways to do it, but I think the simplest is the native React Context API.
All you have to do is create a ContextProvider and then use the useContext hook inside your components to access the values it provides.
Here is an example that should work for your case:
Main.js
export const LoadingContext = React.createContext(true); //creating and exporting the context
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<LoadingContext.Provider value={isLoading}> {/* providing the value to the children */}
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
</LoadingContext.Provider>
);
}
export default Main;
Other components
import {LoadingContext} from './Main.js'
function FirstComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}

React - Is there a way to check when page is done loading after route change?

I have a loading screen and I want to hide it when a page is done loading but I can only do that on the initial page load. I want to display the loading screen when the user changes route and hide it hide it when the page is done loading.
I want something similar to the pace.js library so it can detect when the page is done loading.
If you want to wait some async request for each page, there are few ways:
You can create some global loader and insert it near Router:
<div>
<Loader />
<Switch>
<Route path="/" exact> <Home /> </Route>
<Route path="/users"> <Users /> </Route>
<Route path="/articles"> <Article /> </Route>
...
</Switch>
</div>
Then create custom hook:
// useGlobalLoading.js
import ...
export default (isLoading) => {
const dispatch = ...;
useEffect(() => {
dispatch(showLoading());
return () => {
dispatch(hideLoading());
}
}, [isLoading])
}
and then in any component
...
const Users = () => {
const [isLoading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
useGlobalLoading(isLoading);
const fetch = useCallback(async () => {
try {
const response = await api.getUsers(...);
setUsers(response.data.users);
setIsLoading(false);
} catch (error) { ... }
}, []);
if (isLoading) {
return null;
}
return <div>{users ... }</div>
}
From other side you can create reusable component Loader, and import it to all components where you have to show it:
// Users.js or Articles.js
return (
<>
{isLoading && <Loader />}
{!isLoading && <div>{users...}</div>}
</>
)

A Hooks Context give me en error for loop render why?

I'm beginning to learn React Js. I was trying to create a auth with hooks.
But I recived en error:
Unhandled Rejection (Error): Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside
componentWillUpdate or componentDidUpdate. React limits the number of
nested updates to prevent infinite loops.
This is my code I tried to simplify the components, I hope it's clear
export const AuthContext = React.createContext();
export const AuthProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect( () => {
//const token = localStorage.getItem( 'token' );
//const userName = localStorage.getItem( 'userName' );
console.log('useEffect Auth Provider');
console.log(currentUser);
}, [] );
return (
<AuthContext.Provider
value={
[currentUser, setCurrentUser]
}
>
{children}
</AuthContext.Provider>
);
}
When I try to login in Login.js :
export const Login = () => {
const [ currentUser, setCurrentUser ] = useContext( AuthContext );
// Login
const handleLogin = (event) => {
event.preventDefault();
const { email, password } = event.target.elements;
console.log(email.value, password.value);
const siteUrl = clientConfig.serverUrl;
const loginData = {
email: email.value,
password: password.value
};
axios.post( `${siteUrl}/api/users/login`, loginData )
.then( res => {
setCurrentUser(res.data);
console.log(res.data);
});
}
if (currentUser) {
return <Redirect to="/" />
}
else {
return (
<form onSubmit={handleLogin}>
<input name="email" type="text" placeholder="E-Mail"></input>
<input name="password" type="password" placeholder="**************"></input>
<button type="submit">Login</button>
</form>
);
}
};
App.js:
function App() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route path="*" component={NotFound} />
</Switch>
</Router>
</AuthProvider>
);
}
export default App;
// PrivateRoute
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../context/auth";
export const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const {currentUser} = useContext(AuthContext);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
Where am I wrong? Thanks to anybody who want to help me.
Marco.Italy
Here's a working version of your code:
https://codesandbox.io/s/focused-dubinsky-yhpcl
The problem was in the way you were accessing your current user on your PrivateRoute. It was coming back as undefined.
const { currentUser } = useContext(AuthContext);
You can't destructure an array like that. So I changed to this:
const [currentUser, setCurrentUser] = useContext(AuthContext);
NOTE: I know you don't need the setCurrentUser on PrivateRoute. But it's just a way to make it work clearly as is. You can also do it like this:
const [currentUser] = useContext(AuthContext); // THIS WORKS WHEN YOU'RE GETTING THE FIRST ARRAY VALUE
PrivateRoute.js
export const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
console.log("Rendering PrivateRoute...");
const [currentUser, setCurrentUser] = useContext(AuthContext); // <-------------
console.log("currentUser: " + currentUser);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};

Resources