So I am fetching data from my backend on a landing page in my app. This data is used in different parts, so I use redux for global state. The code is something like this:
//Landing Component
export default function Landing() {
const data = useSelector(state => state.data);
useEffect(() => {
if (!data.isAvailable) {
fetchData();
}
...
}, [])
...
}
//Some Component
export default function SomeComponent() {
const data = useSelector(state => state.data)
...
}
//Another Component
export default function AnotherComponent() {
const data = useSelector(state => state.data)
...
}
However as we know, if the user refreshes the page, the data stored in redux is re-initialized. The problem is, if the user refreshes while on the other pages, data does not get refetched. So what I do is I repeat the same useEffect in every other component that needs the data in order to refetch it in the event the user reloads in a different page.
//Some Component
export default function SomeComponent() {
const data = useSelector(state => state.data)
useEffect(() => {
if (!data.isAvailable) {
fetchData();
}
...
}, [])
...
}
I'm sure there are better ways to do this. I've heard of persisting data through localStorage or using redux-persist. But this data can become quite large so I don't think localStorage will do. Something like a global useEffect that will rerun whatever page the user may be. Thank you!
There is one problem with your approach of calling fetchData in useEffect. If you extend it to multiple components you would end up calling fetchData multiple times.
There isn't a silver bullet solution for this problem. However, one reusable solution would be to dispatch an initializer action and listen in the required reducer using an async thunk and hydrate its own store properties.
Related
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.
I want to achieve the following.
Action from redux is imported in this component. It fetches data from firebase, which I use as a database. Data is displayed inside this component.
My problem is that data is fetched every time component is mounted, which is not an ideal scenario.
I would like to prevent that behavior.
Ideal scenario is that data should be fetched only first time component is rendered and no more afterwards. Of course it should also fetch when data is changed otherwise no. I used useCallback, but certainly there is a mistake in the code below. Token and Id are passed when user is authenticated.
import { fetchFavs } from "../../store/actions";
import { useSelector, useDispatch } from "react-redux";
const token = useSelector((state) => state.auth.token);
const id = useSelector((state) => state.auth.userId);
const fetchedFavs = useSelector((state) => state.saveFavs.fetchedFavs);
const dispatch = useDispatch();
const fetchFavsOptimized = useCallback(() => {
dispatch(fetchFavs(token, id));
console.log("rendered");
}, [token, id, dispatch]);
useEffect(() => {
if (token) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token]);
There are two ways to handle this, one explicit and one implicit.
The explicit way would be to add a new boolean item in your redux store and check against that every time you load your component to see if you should load the data from the db or not.
So your redux store may have a new key/value: dataLoaded: false;
In your component now you need to load this from your redux store and check against it in your use effect:
const dataLoaded = useSelector((state) => state.dataLoaded);
useEffect(() => {
if (token && !dataLoaded) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, dataLoaded]);
Don't forget in your fetchFavsOptimized function to update the dataLoaded to true after the loading of the data from the db is complete.
This has the upside that every time you want to load the db from the db again you simply need to set dataLoaded to false.
The implicit way is to check the data length and if that is 0 then execute the load. Assuming that you store data in your redux store in this form: data: [] and your data is an array (you could do the same thing by initiating data as null and check if data !== null) your component would look like this:
const data = useSelector((state) => state.data);
useEffect(() => {
if (token && data.length === 0) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, data]);
Keep in mind that I wouldn't do it like that because if you don't have any data you may end up with an infinite loop, but it's a good technique to use in other cases.
For fetching only once on component mounting: (mind [])
React.useEffect(() => {
fetchFavsOptimized()
}, []) // <- this empty square brackets allow for one execution during mounting
I'm not sure, but the issue of continous calling of useEffect might be [fetchFavsOptimized, token]. Placing fetchFavsOptimized function in [] might not be the best way.
You want to fetch, when data changes, but what data? You mean token ? Or something else?
React.useEffect() hook alows for fetching when data changes with placing this changable data in [here_data] of useEffect. Example:
const [A, setA] = React.useState();
React.useEffect(() => {
// this block of code will be executed when state 'A' is changed
},[A])
The best practise of using useEffect is to separate the conditions in runs with.
If You need to fetch on some data change AND on component mount, write TWO useEffect fuctions. Separate for data change and component mount.
After some time working with react-redux and redux thunk I have realise about a behaviour, which isnt the best user experience.
I know that when you are working with react and you are fetching data in useEffect when the component is rendering and for any reason you go back or navigate somewhere else you need to clear the state with a function in the return (which will recreate the componentWillUnmount lifecycle)
This problem I am facing however occurs when working with redux thunk because the data fetch is with the actions creators. So to make my long story short I will show some code. The fetching action looks something like this:
export const fetchData = () => async (dispatch, getState) => {
try {
dispatch(fetchDataStart())
const response = await fetch('https://jsonplaceholder.typicode.com/photos')
dispatch(fetchDataSucess(data))
} catch (error) {
console.log(error)
}
}
Let's say that I call this action in my component useEffect like this:
useEffect(() => {
const loadData = async () => {
await dispatch(fetchData())
}
loadData()
return () => dispatch(resetData())
},[ ])
As you can see I am dispatching a resetData action to clear the state when the component unmounts BUUUUT this is where the problem arrives. If before fetching the data the user navigates to another page, the resetData will be dispatched BUT as the fetch could not be finished the data will be stored after having been reseted. So when the user navigates back to that component it will blink (show only very quickly, maybe for a second) the old data before loading the new one. So is there any way to avoid this problem with redux thunk?
PD: I could block the navigation or the whole screen with a backdrop so the user wont navigate until the fetch is finished but i feel like that is kind of a workaround of the problem. However, let me know if you think that this would still be the best way.
Thank you.
You can create an AbortController instance. That instance has a signal property, and we pass the signal as a fetch option. Then to cancel data fetching we call the AbortController's abort property to cancel all fetches that use that signal.
export const fetchData = () => async (dispatch) => {
try {
const controller = new AbortController();
const { signal } = controller;
dispatch(fetchDataStart(controller)); // save it to state to call it later
const response = await fetch('https://jsonplaceholder.typicode.com/photos', { signal });
dispatch(fetchDataSucess(data));
} catch (error) {
console.log(error);
}
}
useEffect:
useEffect(() => {
dispatch(fetchData());
return () => dispatch(resetData()) // resetData will call controller.abort() that was saved in state
},[ ])
I have a simple shopping site, with the data coming from a headless CMS system. I am using nextJS/React to build out the site. When a user adds items to the cart, I want to use firebase to persist the state, as the user navigates different pages. However, I lose all data in the state as well as firebase, when the user navigates to different pages. The main problem is I can't set the initial cart state to the firebase data. How do I solve this problem, so i can use the firebase data to persist cart state?
import React, { useState, useEffect } from 'react';
import Layout from './../components/Layout';
import contextCart from './../components/contextCart';
import firebase from '../data/firestore';
export default function MyApp({ Component, pageProps }) {
//any way to set the initial cart state to data from firebase???
const [ cart, setCart ] = useState([]);
const addToCart = (product) => {
setCart((prevCartState) => {
return [ ...prevCartState, product ];
});
firebase.database().ref().set(cart);
};
//componentDidUpdate => listen for cart changes and update the localStorage
useEffect(
() => {
firebase.database().ref().set(cart);
},
[ cart ]
);
//componentDidMount => run the effect only once and run cleanup code on unmount
useEffect(() => {
//effect code
firebase.database().ref().once('value').then(function(snapshot) {
setCart(snapshot.val() || []);
});
//returned function will be called on component unmount aka cleanup code
//return () => { /*any cleanup code*/ };
}, []);
return (
<contextCart.Provider value={{ cart: cart, addToCart: addToCart }}>
<Layout>
<Component {...pageProps} />
</Layout>
</contextCart.Provider>
);
}
The data is not updating because you are not listening to the changes in your code.
You specify to get the data once in the useEffect Hook , which only runs on the initial load.
To listen to changes you can implement it like this:
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', function(snapshot) {
updateStarCount(postElement, snapshot.val());
});
read more about it in the official firebase docs here: https://firebase.google.com/docs/database/web/read-and-write
I would recommend you to use a state-management library like Redux or even the Context API from React itself.
The reason for this is that firebase charge you for the traffic to your database (this is a bigger issue for the firestore db than the real-time db but still there are costs to consider).
With a state library, you can check if you need to fetch the data from firebase or if you already have what you need in your state.
Also if you can split your code up with the Provider Pattern, this makes it easier to maintain the logic while your application is growing.
What is the correct way to get state from the Redux store within the useEffect hook?
useEffect(() => {
const user = useSelector(state => state.user);
});
I am attempting to get the current state within useEffect but I cannot use the useSelector call because this results in an error stating:
Invariant Violation: Hooks can only be called inside the body of a function component.
I think I understand why as it breaks one of the primary rules of hooks.
From reviewing the example on the Redux docs they seem to use a selectors.js file to gather the current state but this reference the mapStateToProps which I understood was no longer necessary.
Do I need to create some kind of "getter" function which should be called within the useEffect hook?
Don't forget to add user as a dependency to useEffect otherwise your effect won't get updated value.
const user = useSelector(state => state.user);
useEffect(() => {
// do stuff
}, [user]);
You can place useSelector at the top of your component along with the other hooks:
const MyComponent = () => {
...
const user = useSelector(state => state.user);
...
}
Then you can access user inside your useEffects.
I found using two useEffects to works for me, and have useState to update the user (or in this case, currUser).
const user = useSelector(state=>state.user);
const [currUser, setCurrUser] = useState(user);
useEffect(()=>{
dispatch(loadUser());
}, [dispatch]);
useEffect(()=>{
setCurrUser(user);
}, [user]);
You have to use currUser to display and manipulate that object.
You have two choices.
1 - If you only need the value from store once or 'n' time your useEffect is called and don't want to listen for any changes that may occur to user state from redux then use this approach
//import the main store file from where you've used createStore()
import {store} from './store' // this will give you access to redux store
export default function MyComponent(){
useEffect(() =>{
const user = store.getState().user;
//...
},[])
}
2 - If you want to listen to the changes that may occur to user state then the recommended answer is the way to go about
const MyComponent = () => {
//...
const user = useSelector(state => state.user);
useEffect(() => {
//...
},[])
//...
}
const tournamentinfofromstore=useSelector(state=>state.tournamentinfo)
useEffect(() => {
console.log(tournamentinfofromstore)
}, [tournamentinfofromstore])
So the problem is that if you change the state inside the useEffect that causes a rerender and then again the useEffect gets called "&&" if that component is passing data to another component will result in infinite loops.and because you are also storing that data in the child component's state will result in rerendering and the result will be infinite loop.!!
Although it is not recommended, you can use store directly in your component, even in the useEffect.
First, you have to export store from where it is created.
import invoiceReducer from './slices/invoiceSlice';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
invoices: invoicesReducer,
auth: authReducer,
},
});
Then you can import it to a React Component, or even to a function, and use it.
import React, { useEffect } from 'react';
import { store } from './store';
const MyComponent = () => {
useEffect(()=> {
const invoiceList = store.getState().invoices
console.log(invoiceList)
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default MyComponent
You can study the API for Store in here.
You can also see why this approach is not recommended in
here.
Or, if you are interested in using redux store outside a react component, take a look at this blog post.
To add on top of #Motomoto's reply. Sometimes you depend on store to be loaded before useEffect. In this case you can simply return in if the state is undefined. useEffect will rerender once the store is loaded
const user = useSelector(state => state.user);
useEffect(() => {
if(user === undefined){
return}else{
// do stuff
}}, [user]);
I'm having the same issue, The problem to the useSelector is that we cant call it into the hook, so I can't be able to update with the action properly. so I used the useSelector variable as a dependency to the useEffect and it solved my problem.
const finalImgData_to_be_assigned = useSelector((state) => state.userSelectedImg);
useEffect(()=>{
console.log('final data to be ready to assign tags : ', finalImgData_to_be_assigned.data);
}, [finalImgData_to_be_assigned ])