React / Redux: why useEffect is called too many times? - reactjs

First of all, I'm sorry if the first question is unfamiliar and difficult to understand
Purpose:
Get data from firestore and fetch again when list of store is updated.
add info.
I want to get data from a firestore and redraw the screen when the list of stores is updated.
This is a general list update.
problem:
useEffect wants to execute fetch when the value of stores is updated, but useEffect is always called and fetch request to firestore does not stop.
The code is below.
Test.jsx
...
import { useDispatch, useSelector } from "react-redux";
import { fetchStores } from "../../reducks/stores/operations";
const storesRef = db.collection('stores');
const Test = () => {
const dispatch = useDispatch();
const selector = useSelector(state=>state);
// stores set initialState
// stores: {
// name: "",
// list: [],
// }
const stores = selector.stores.list;
useEffect(()=>{
dispatch(fetchStores())
},[stores])
}
...
reducks/stoers/operations.js
export const fetchStores = () => {
return async (dispatch) => {
storesRef.where().get()
.then(snapshots => {
const storeList = []
snapshots.forEach(snapshot => {
const store = snapshot.data();
store['id'] = snapshot.id;
storeList.push(store)
})
dispatch(fetchStoresAction(storeList))
})
}
}
*fetchStoresAction saves an array in the store list.
thank you

You should have an empty dependency array in useEffect so the fetch request only executed once when the components gets mounted:
useEffect(() => {
dispatch(fetchStores())
}, [])
If you would like to fetch stores when new store is created or updated, you should do it in a function where you can access the update or create fetch request's result. If the result is successful (successful update on database), then you can fetch your stores again.

Related

Pull data from firestore using useEffect works on re-render only

Here is my code:
import React, { useEffect, useState } from 'react';
import { getDocs, collection } from 'firebase/firestore';
import { db } from '../firebase-config';
const Home = () => {
const [postList, setPostList] = useState([]);
const postsCollectionRef = collection(db, "data");
useEffect(() => {
const getPosts = async () => {
const data = await getDocs(postsCollectionRef);
let postListArray = []
data.forEach((doc) => {
const post = { ...doc.data() }
postListArray.push(post)
});
setPostList(postListArray)
};
getPosts();
console.log(postList);
}, []);
return (
<div>test</div>
);
};
export default Home;
On loading, the console.log returned an empty array. The spooky thing is when i changed anything , for example
return (
<div>test_epic</div>
);
The console.log shows that it is an array. Anyone has any idea as to why? Please refer to the screepcap as attached.
the first render on loading
I changed anything and components rerendered
Setting state in react is asynchronous, so the data is loaded and the state is set but the console.log statement is executed before the setting state async operation is complete
To make it a bit more clear this is how it works step by step
Component is rendered and postList is initialized with a value of []
useEffect is triggered
Data is fetched
A call to set a new value of postList is placed using setPostList (key here is a call is placed not that the data is actually updated)
You print console.log with a value from Step 1
The call from Step 4 is complete and now the data is actually updated
Here is an article that explains it with examples
And here is another answer that explains this deeply

The problem with useState that the data goes to the firestore empty data?

I'm aiming to update the data in the database but every time I send the data with useState it sends blank data.
Also, as I don't understand, when I print the firebase information to the console, it appears as +1 added. So it looks up-to-date but not in the database. What is the reason for this?
export default postDetails = ({navigation, route}) => {
const levelColl = firestore().collection('Level');
const [levelData,setLevelData] = useState([]);
function StoreLevelData (){
levelColl
.doc(user)
.get()
.then(documentSnapshot=>{
data=documentSnapshot.data();
labels=data.levelList.labels;
data1=data.levelList.data;
console.log(data)
let levelList1=[];
for (let index = 0; index < labels.length; index++) {
//console.log(labels[index],data[index])
if(labels[index] == 'araba'){
data1[index]+=0.0051
console.log(data1[index])
}
levelList1.push({
levelList:{
labels:[labels[index]],
data:[data1[index]]
}
})
}
console.log(levelList1)
setLevelData(levelList1)
})
}
levelColl
.doc(user)
.set({
levelData})
useEffect(() => {
StoreLevelData();
} , []);
enter image description here
You cannot set the state and then immediately access the new data, because in react setState is asynchronous. In order for you to access the newly set state data, you will have to wait for the next re-render.
So in your case the best this to do is to create a new function below like
const updateLevelList = (levelListData) => {
setLevelData(levelListData) // Update the state here
levelColl.doc(user)
.set({
levelListData // Use the same function arg instead of using state
})
}
And the call this function where you used to set the state, ie replace setLevelData(levelList1) with updateLevelList(levelList1)

Update single element of the array in redux state

I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.

useSelector is not working, redux in react

When I use useSelector the variable is always holding its initial state. I have the feeling it is stored in some parallel galaxy and never updated. But when I retrieve the value with const store = useStore(); store.getState()... it gives the correct value (but lacks subscribtions). When I inspect the store in redux devtools I can see all the values are recorded in the store correctly. Values are just not retrieved from the store with useSelector.
What I wanted to achieve is to have some cache for user profiles, i.e. not fetch /api/profile/25 multiple times on the same page. I don't want to think of it as "caching" and make multiple requests just keeping in mind the requests are cached and are cheap but rather thinking of it as getting profiles from the store and keeping in mind profiles are fetched when needed, I mean some lazy update.
The implementation should look like a hook, i.e.
// use pattern
const client = useProfile(userId);
// I can also put console.log here to see if the component is getting updated
let outputProfileName;
if( client.state==='pending' ) {
outputProfileName = 'loading...';
} else if( client.state==='succeeded' ) {
outputProfileName = <span>{client.data.name}</span>
} // ... etc
so I placed my code in use-profile.js, having redux-toolkit slice in profile-slice.js
profile-slice.js
import {
createSlice,
//createAsyncThunk,
} from '#reduxjs/toolkit';
const entityInitialValue = {
data: undefined,
state: 'idle',
error: null
};
export const slice = createSlice({
name: 'profile',
initialState: {entities:{}},
reducers: {
updateData: (state,action) => {
// we received data, update the data and the status to 'succeeded'
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: action.payload.data,
state: 'succeeded',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchPendStart: (state,action) => {
// no data - indicates we started fetching
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: null,
state: 'pending',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchError: (state,action) => {
state.entities[action.payload.id] = {
//...entityInitialValue,
...state.entities[action.payload.id],
data: null,
state: 'failed',
error: action.payload.error
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
},
extraReducers: {
}
});
export const {updateData,dispatchPendStart,dispatchError} = slice.actions;
// export const selectProfile... not used
export default slice.reducer;
use-profile.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector, useStore } from 'react-redux';
import {
updateData as actionUpdateData,
dispatchPendStart as actionDispatchPendStart,
dispatchError as actionDispatchError,
} from './profile-slice';
//import api...
function useProfile(userId) {
const dispatch = useDispatch();
const actionFunction = async () => {
const response = await client.get(`... api endpoint`);
return response;
};
const store = useStore();
// versionControl is a dummy variable added for testing to make sure the component is updated;
// it is updated: I tried adding console.log to my component function (where I have const client = useProfile(clientId)...)
const [versionControl,setVersionControl] = useState(0);
const updateVersion = () => setVersionControl(versionControl+1);
// TODO: useSelector not working
const updateData = newVal => { dispatch(actionUpdateData({id:userId,data:newVal})); updateVersion(); };
const dispatchPendStart = newVal => { dispatch(actionDispatchPendStart({id:userId})); updateVersion(); };
const dispatchError = newVal => { dispatch(actionDispatchError({id:userId,error:newVal})); updateVersion(); };
const [
getDataFromStoreGetter,
getLoadingStateFromStoreGetter,
getLoadingErrorFromStoreGetter,
] = [
() => (store.getState().profile.entities[userId]||{}).data,
() => (store.getState().profile.entities[userId]||{}).state,
() => (store.getState().profile.entities[userId]||{}).error,
];
const [
dataFromUseSelector,
loadingStateFromUseSelector,
loadingErrorFromUseSelector,
] = [
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].data : undefined ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingError : undefined ),
];
useEffect( async () => {
if( !(['pending','succeeded','failed'].includes(getLoadingStateFromStoreGetter())) ) {
// if(requestOverflowCounter>100) { // TODO: protect against infinite loop of calls
dispatchPendStart();
try {
const result = await actionFunction();
updateData(result);
} catch(e) {
dispatchError(e);
throw e;
}
}
})
return {
versionControl, // "versionControl" is an approach to force component to update;
// it is updating, I added console.log to the component function and it runs, but the values
// from useSelector are the same all the time, never updated; the problem is somewhere else; useSelector is just not working
// get data() { return getDataFromStoreGetter(); }, // TODO: useSelector not working; but I need subscribtions
// get loadingState() { return getLoadingStateFromStoreGetter(); },
// get loadingError() { return getLoadingErrorFromStoreGetter(); },
data: dataFromUseSelector,
loadingState: loadingStateFromUseSelector,
loadingError: loadingErrorFromUseSelector,
};
}
export default useProfile;
store.js
import { configureStore,combineReducers } from '#reduxjs/toolkit';
import profileReducer from '../features/profile/profile-slice';
// import other reducers
export default configureStore({
reducer: {
profile: profileReducer,
// ... other reducers
},
});
component.js - actually see the use pattern above, there's nothing interesting besides the lines posted.
So
When I export loading state (I mean last lines in use-profile.js; I can suppress last three lines and uncomment the other three). So, if I use getLoadingStateFromStoreGetter (values retrieved via store.getState()...), then some profile names are displaying names that were fetched and some are holding "loading..." and are stuck forever. It makes sense. The correct data is retrieved from redux store and we have no subscribtions.
When I export the other version, created with useSelector, I always get its initial state. I never receive any user name or the value indicating "loading".
I have read many answers on StackOverflow. Some common mistakes include:
Some are saying your component is not getting updated. It's not the case, I tested it placing console.log to the code and adding the versionControl variable (see in the code) to make sure it updates.
Some answers are saying you don't update the store with reducers correctly and it still holds the same object. It's not the case, I tried both approaches, to return a fresh new object {...state,entities:{...state.entities...etc...}} and mutating the existing proxy object - both way my reducers should provide a new object and redux should notify changes.
Sometimes multiple store instances are created and things are messed. It's definitely not the case, I have a single call to configureStore() and a single component.
Also I don't see hook rules violation in my code. I have an if statement inside the useSelector fn but the useSelector hook itself is called unconditionally.
I have no idea what other reasons are causing useSelect to simply not work. Could anyone help me understand?
Ops, as usual, very simple typo is the reason. So many hours spent. Very sorry to those who have spent time trying to look at this and thanks for your time.
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' )
There should be not .loadingState but .state. That's it.

how to make an object with multiple object an array in state?

This is my code,
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
function App() {
let [albums, setAlbums] = useState([]);
useEffect(() => {
const key = "blablabla to keep secret";
const fetchData = async () => {
const result = await axios(
`http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=cher&api_key=${key}&limit=10&format=json`
);
setAlbums(result.data.topalbums);
console.log(albums, "data?");
// const { data } = props.location.state;
};
fetchData();
}, []);
return <div className="App"></div>;
}
export default App;
the data I am fetching is in an object with objects inside, in-state I initialized with an empty array, and then after I fetched the data I use setAlbums. I have two problems, the console.log after I fetch just shows me an empty array, but when I console log in render I do get the data but it is an object and not an array of objects, so I can't even map over it, how do I fix this?
Try to do something like this:
setAlbums(album => [...album, results.data.topalbums])
that way you can push results to your array instead of transforming it into object
also if you wish to see updated album then create something like:
useEffect(() => {
console.log(albums)
},[albums])
as setting state is asynchronous therefore it doesn't happen immediately as Brian mentioned, so this code will launch if albums state changes its value

Resources