How to make setting state with useEffect() to run on page refresh? - reactjs

My code is not long or complicated at all. It's simple. so please read!
(Im using react + next.js)
In the root file, app.js, I have useEffect to fetch photo data. This data array will be used in a page component so I pass it down from app.js via <Component.../>
function MyApp({ Component, pageProps }) {
const [photoData, setPhotoData] = useState([]);
const [user, setUser] = useState([]);
useEffect(() => {
const getPhotos = async () => {
try {
const photoData = await axios.get(
"https://jsonplaceholder.typicode.com/albums"
);
setPhotoData(photoData.data);
} catch (error) {
console.log(error);
}
};
getPhotos();
}, []);
useEffect(() => {
//code for finding user. no external api used.
setUser(user);
}
}
}, []);
const passedProps = {
...pageProps,
photoData,
user
};
return (
...
<Component {...passedProps} />
)
Then I pass the data (photodata) from a Home component to a (app.js 's) grandchild component, an Photo component
export default function Home({ photoData, user }) {
return(
<Photo photoData={photoData} user={user} />
)
In Photo component, I am receiving photoData and trying to set a state for photoArr with the default state of photoData.
When the entire app is first loaded, the photoData is passed down to the Photo component successfully that it sets the state without any issue.
But the main problem is that when I am in the Photo page (photos are loaded) and refresh the page, then it does not set the state for photoArr with photoData. Even though I can console log photoData received from app.js, it does not set state, photoArr, with the default state, photoData.
export default function Photo({ photoData, user }) {
const [photoArr, setPhotoArr] = useState(photoData);
//I have this as state because I change this array
//later on in this component (doing CRUD on the photo array).
console.log(photoData); // this returns the array received from app.js
console.log(photoArr); // []. returns an empty array
console.log(user); // returns the user object received from app.js.
return (
<div>
{photoArr.length > 0 ?
.... code mapping photoArr
: "Data is empty" //It returns this comment when I refresh the page
}
</div>
)
As you can see above, when I refresh the page, I get "Data is empty" meaning photoArr was not set even with the given default state. If I keep refreshing the page multiple times, it still shows a blank page.
From my research, it's due to setting state being asynchronous? So then how can I fix this problem?

Try this:
(In your Photo page)
const [photoArr, setPhotoArr] = useState(null);
useEffect(() => {
if(photoData.length) setPhotoArr(photoData) // If not empty, set the Arr
},[photoData]} // We listen to photoData's change
On page load, there aren't any data in your photoData, and as it pass down to Photo component, react remembers that state.
But with useEffect listen to photoData's change, we can setPhotoArr once the getPhotos function got the data back.

Related

react-router-dom state returns error on page refresh

I am new to react-router-dom, I was calling Data inside
of the ParentPage.jsx and mapped it using Card.jsx and it returned cards of data. In the
Card.jsx I passed the data to the ChildPage page using <Link/> and it worked, but if I'm going to refresh the child page it returns an error TypeError: Cannot read properties of undefined. I have also tried storing the data on the localStorage but it is still returning the same error. I hope someone can help me.
Here are my code snippets.
ParentPage.jsx
const [establishment, setEstablishment] = useState([]);
const Data = () => {
...
};
const cards = useMemo(() => {
return establishment.map((establishment) => (
<Card establishment={establishment} />
));
});
...
{cards}
...
Card.jsx
const [details] = useState(establishment);
return (
<>
<text>{details.name}</text>
<Link
to={{
pathname: "/establishments/details",
state: { details },
}}
>
<Button>
Details
</Button>
</Link>
</>
);
ChildPage.jsx
const {state} = useLocation();
const [data, setData] = useState(state?.details);
useEffect(() => {
window.localStorage.setItem(data.id, JSON.stringify(data));
}, [data]);
useEffect(() => {
const updatedData = window.localStorage.getItem(data.id);
if (updatedData !== null) setData(JSON.parse(updatedData));
}, []);
...
...data.color
...
Here is the Error
Issue
You are on the right track, but the logic is a little mixed up. Route state is very transient, it only exists during the route transition and while the receiving component remains mounted. Reloading the page reloads the entire React app. Any state in memory is lost.
Current code:
const location = useLocation();
const [data, setData] = useState(location.state?.details); // (A)
useEffect(() => {
window.localStorage.setItem(data.id, JSON.stringify(data)); // (B)
}, [data]);
useEffect(() => {
const updatedData = window.localStorage.getItem(data.id);
if (updatedData !== null) setData(JSON.parse(updatedData));
}, []);
...
.... data.color // (C)
...
Here's what I see occurring:
Navigate to child page with defined state, the data state is initialized to location.state.details (A), and the component renders with defined data.color (C). No error.
The first useEffect hook runs and persists the data state to local storage under the defined key data.id (B).
The second useEffect hook runs and reads from localStorage using defined data.id key and since it's not null enqueues a data state update.
Reload the page.
The app remounts. This ChildPage component remounts. The data state is initialized to the undefined location.state value (A). Error thrown accessing data.color on the initial render (C).
Solution
The data state should be initialized from location.state.data if it exists, then fallback to localStorage if it exists, then possibly fallback to a base value. Use only a single useEffect hook to persist the local state to localStorage when it updates. Use a storage key that is always defined.
const { state } = useLocation();
const [data, setData] = useState(() => {
return state?.details || JSON.parse(window.localStorage.getItem("details")) || {};
});
useEffect(() => {
window.localStorage.setItem("details", JSON.stringify(data));
}, [data]);
...
.... data.color
...
I think the problem is with the parsing of data not the link it self. It would have helped a lot if you had showed some code and the full error message
For this kind of usage, the best practice would be using query param in react-router-dom, so that you can pass your value and by refreshing the page will work the same, you can check if there is no query param in the child component you can redirect the user back
I think this blog will help to handle it https://denislistiadi.medium.com/react-router-v6-fundamental-url-parameter-query-strings-customizing-link-57b75f7d63dd

React Firebase read data results in too many renders

I'm using Firebase realtime database for my react project. I try to follow the firebase documentation and use "onValue()" to retrieve data. Here is my code:
export default function Home() {
const {currentUser} = useAuth();
const [userinfo,setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
setUserinfo(data);
})
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
This would result in an error of too many re-renders. I don't know how to retrieve the data. If I use
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
console.log(data);
})
then the proper data would print out in the console perfectly fine. I also tried the following:
let info;
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
info = data;
})
console.log(info)
but info would just be undefined. I can't seem to figure out the problem here. How can I use the data?
It throws error too many re-renders because you are not using any lifecycle hook or function to update/change state value and once you update your state it will again re-render your whole component and then again you update the state and the same thing happens in the loop causing too many re-renders.
So to avoid this you need to put code that is responsible for listening to changes from DB and changing state inside a block which will only get called on specific events or function calls or etc.
In your case, I suggest using useEffect hook. see below code -
export default function Home() {
const { currentUser } = useAuth();
const [userinfo, setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
// this useEffect will get called only
// when component gets mounted first time
useEffect(() => {
// here onValue will get initialized once
// and on db changes its callback will get invoked
// resulting in changing your state value
onValue(ref(db, `users/${uid}`), snapshot => {
const data = snapshot.val();
setUserinfo(data);
})
return () => {
// this is cleanup function, will call just on component will unmount
// you can clear your events listeners or any async calls here
}
}, [])
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
Note - I have not worked with firebase real-time DB recently but by looking at the code and error I have added this answer, let me know if anything needs correction.

React component content disappears after page refresh

I am new to react and am having trouble figuring out why the data inside my Content component does not re-render on refresh.
When I visit one of the routes, say /sentences-of-the-day, and then I refresh the page, it seems all the stuff inside content is gone.
Can someone please help me out?
Here is the code sandbox:
https://codesandbox.io/s/mainichome-v7hrq
You need to load the data once the component is mounted (using useEffect) set to local state to trigger the render. In each refresh, mounting happens again and you have the data after each refresh.
Define another function in content.data.js
export const getContentData = () => {
return Promise.all(contentDataStories.map((func) => func()));
};
In your content.component.jsx
import { getContentData } from "./content.data.js";
const [content, setContent] = useState([]);
useEffect(() => {
(async () => {
setContent(await getContentData());
})();
}, []);
Code sandbox => https://codesandbox.io/s/mainichome-forked-4sx5n?file=/src/components/content/content.component.jsx:302-449
The problem is here:
import contentData from "./content.data.js";
//...
const [content] = useState(contentData);
That imports contentData and then sets it as state.
However, that value is asynchronous.
const contentData = [];
contentDataStories.forEach(function (func) {
func().then((json) => {
contentData.push(json);
});
});
export default contentData;
It's just [] until those promises reoslve.
So what's happening is that the page is loading fine, but the content from that file hasn't loaded before the first render.
This is a race condition. Which will happen first, the data loading or the render? Sometimes the render wins and everything is fine, but sometimes it doesn't and you get a blank page.
To fix it, you need to make React aware of your data loading, so that it can re-render when the data finishes loading.
First make a function that does your async loading:
export function getContentData() {
return new Promise((resolve) => {
// fetch async stuff here
resolve(myDataHere)
})
}
And then call that from a useEffect, which sets the state.
function Content() {
const { titleParam } = useParams();
const [content, setContent] = useState(contentData);
useEffect(() => {
getContentData().then(setContent);
}, [getContentData]);
//...
}
Now when you component mounts, it calls getContentData. And then that promise resolves, it sets the state, triggering a a new render.

Fetching w/ custom React hook based on router parameters

I'm trying to fetch data with a custom React hook, based on the current router parameters.
https://stackblitz.com/edit/react-fetch-router
What it should do:
On first load, check if URL contains an ID...
If it does, fetch a todo with that ID
If it does not, fetch a todo with a random ID & add ID to url
On fetch button clicks...
Fetch a todo with a random ID & add ID to url
What is wrong:
Watching the console or inspector network tab, you can see that it's firing several fetch requests on each click - why is this and how should this be done correctly?
Since you used history.push on handleClick, you will see multiple requests being sent as you are using history.push on click handler which will cause a re-render and make use use random generated url as well as also trigger the fetchTodo function.
Now a re-render will occur which is cause a randomised id to be generated. and passed onto useTodo hook which will lead to the fetchTodo function being called again.
The correct solution for you is to set the random todo id param on handleClick and also avoid unnecessary url updates
const Todos = () => {
const history = useHistory();
const { id: todoId } = useParams();
const { fetchTodo, todo, isFetching, error } = useTodo(todoId);
const isInitialRender = useRef(true);
const handleClick = () => {
const todoId = randomMax(100);
history.push(`/${todoId}`);
};
useEffect(() => {
history.push(`/${todoId}`);
}, []);
useEffect(() => {
if(!isInitialRender.current) {
fetchTodo();
} else {
isInitialRender.current = false
}
}, [todoId])
return (
<>
<button onClick={handleClick} style={{marginBottom: "10px"}}>Fetch a todo</button>
{isFetching ? (
<p>Fetching...</p>
) : (
<Todo todo={todo} color={todo.color} isFetching={isFetching} />
)
}
</>
);
};
export default Todos;
Working demo

Component shows previous data when mount for fractions of seconds

I am developing an app named "GitHub Finder".
I am fetching the date in App component using async function and pass these function to User component as props and I call these functions in useEffect.
The problem is here, when I goto user page for second time it shows previous data which I passed in props from App component and then it shows loader and shows new data.
Here is App component code where I am fetching date from APIs and passing to User component through props.
// Get single GitHub user
const getUser = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}
// Get user repos
const getUserRepos = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}/repos?
per_page=5&sort=created:asc&client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setRepos(res.data);
setLoading(false);
}`
User component code.
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
}, []);
I've recorded a video, so you guys can easily understand what I am trying to say.
Video link
Check live app
How can I solve this problem?
Thank in advance!
Here is what happens in the app :
When the App component is rendered the first time, the state is user={} and loading=false
When you click on a user, the User component is rendered with props user={} and loading=false, so no spinner is shown and no data.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user1 and set loading=false (now the user data is rendered)
When you go back to search page, the app state is still user=user1 and loading=false
Now when you click on another user, the User component is rendered with props user=user1 and loading=false, so no spinner is shown and the data from previous user is rendered.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user2 and set loading=false (now the new user data is rendered)
One possible way to fix this problem :
instead of using the loading boolean for the User component, inverse it and use loaded
When the User component is unmounted clear the user data and the loaded boolean.
App component:
const [userLoaded, setUserLoaded] = useState(false);
const getUser = async username => {
await setUserLoaded(false);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
await setUser(res.data);
setUserLoaded(true);
};
const clearUser = () => {
setUserLoaded(false);
setUser({});
};
<User
{...props}
getUser={getUser}
getUserRepos={getUserRepos}
repos={repos}
user={user}
loaded={userLoaded}
clearUser={clearUser}
/>
User component:
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
return () => clearUser();
}, []);
if (!loaded) return <Spinner />;
You can find the complete code here
Please make your setUser([]) empty at the start of getUser like this:
const getUser = async (username) => {
setLoading(true);
setUser([]);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}

Resources