useEffect and redux a cache trial not working - reactjs

I have a rather complex setup and am new to React with Hooks and Redux.
My setup:
I have a component, which when first mounted, should fetch data. Later this data should be updated at a given interval but not too often.
I added useRef to avoid a cascade of calls when one store changes. Without useEffect is called for every possible change of the stores linked in its array.
The data is a list and rather complex to fetch, as I first have to map a name to an ID and
then fetch its value.
To avoid doing this over and over again for a given "cache time" I tried to implement a cache using redux.
The whole thing is wrapped inside a useEffect function.
I use different "stores" and "reducer" for different pieces.
Problem:
The cache is written but during the useEffect cycle, changes are not readable. Even processing the same ISIN once again the cache returns no HIT as it is empty.
Really complex implementation. It dawns on me, something is really messed up in my setup.
Research so far:
I know there are libs for caching redux, I do want to understand the system before using one.
Thunk and Saga seem to be something but - same as above plus - I do not get the concept and would love to have fewer dependencies.
Any help would be appreciated!
Component - useEffect
const calculateRef = useRef(true);
useEffect(() => {
if (calculateRef.current) {
calculateRef.current = false;
const fetchData = async (
dispatch: AppDispatch,
stocks: IStockArray,
transactions: ITransactionArray,
cache: ICache
): Promise<IDashboard> => {
const dashboardStock = aggregate(stocks, transactions);
// Fetch notation
const stockNotation = await Promise.all(
dashboardStock.stocks.map(async (stock) => {
const notationId = await getXETRANotation(
stock.isin,
cache,
dispatch
);
return {
isin: stock.isin,
notationId,
};
})
);
// Fetch quote
const stockQuote = await Promise.all(
stockNotation.map(async (stock) => {
const price = await getQuote(stock.notationId, cache, dispatch);
return {
isin: stock.isin,
notationId: stock.notationId,
price,
};
})
);
for (const s of dashboardStock.stocks) {
for (const q of stockQuote) {
if (s.isin === q.isin) {
s.notationId = q.notationId;
s.price = q.price;
// Calculate current price for stock
if (s.quantity !== undefined && s.price !== undefined) {
dashboardStock.totalCurrent += s.quantity * s.price;
}
}
}
}
dispatch({
type: DASHBOARD_PUT,
payload: dashboardStock,
});
return dashboardStock;
};
fetchData(dispatch, stocks, transactions, cache);
}
}, [dispatch, stocks, transactions, cache]);
Action - async fetch action with cache:
export const getXETRANotation = async (
isin: string,
cache: ICache,
dispatch: AppDispatch
): Promise<number> => {
Logger.debug(`getXETRANotation: ${isin}`);
if (CACHE_ENABLE) {
const cacheTimeExceeding = new Date().getTime() + CACHE_NOTATION;
if (
isin in cache.notation &&
cache.notation[isin].created < cacheTimeExceeding
) {
Logger.debug(
`getXETRANotation - CACHE HIT: ${isin} (${cache.notation[isin].created} < ${cacheTimeExceeding} current)`
);
return cache.notation[isin].notationId;
}
}
// FETCH FANCY AXIOS RESULT
const axiosRESULT = ...
if (CACHE_ENABLE) {
Logger.debug(`getXETRANotation - CACHE STORE: ${isin}: ${axiosRESULT}`);
dispatch({
type: CACHE_NOTATION_PUT,
payload: {
isin,
notationId: axiosRESULT,
created: new Date().getTime(),
},
});
}
//FANCY AXIOS RESULT
return axiosRESULT;
}

Related

Why is React Native AsyncStorage not updating state on mount?

When I try to load the state from AsyncStorage for the screen I just navigated to, I am getting this error:
TypeError: undefined is not an object (evaluating 'weights[numExercise].map') It is trying to use the initial state that the screen initializes the state with, but I want the state to be loaded with the data that I specifically try to load it with on mount, within my useEffect hook.
const WorkoutScreen = ({ navigation, route }) => {
const [workoutName, setWorkoutName] = useState("");
const [exercisesArr, setExercisesArr] = useState([""]);
// Each array inside the arrays (weights & reps), represents an exercise's sets.
const [weights, setWeights] = useState([[""]]);
const [reps, setReps] = useState([[""]]);
const [restTimers, setRestTimers] = useState([""]);
useEffect(() => {
try {
console.log("loading workoutscreen data for:", route.params.name);
const unparsedWorkoutData = await AsyncStorage.getItem(route.params.name);
if (unparsedWorkoutData !== null) {
// We have data!
const workoutData = JSON.parse(unparsedWorkoutData);
setWorkoutName(route.params.name.toString());
setExercisesArr(workoutData[0]);
setWeights(workoutData[1]);
setReps(workoutData[2]);
setRestTimers(workoutData[3]);
}
} catch (error) {
// Error retrieving data
console.log("ERROR LOADING DATA:", error);
}
}, []);
Then further down the line in a component it realizes the error because, again, it's using the initialized state for the weights state.
Return (
{weights[numExercise].map((weight, i) => {
return (
<SetComponent
key={i}
numSet={i}
numExercise={numExercise}
prevWeights={prevWeights}
weights={weights}
setWeights={setWeights}
prevReps={prevReps}
reps={reps}
setReps={setReps}
isDoneArr={isDoneArr}
setIsDoneArr={setIsDoneArr}
/>
);
})}
);
I've made sure that the data is being stored, loaded, and used correctly, so (I think) I've narrowed it down to be something asynchronous; whether it's the setting of the state or loading from storage, I don't know and I can't find a solution. I am new to React Native and would love some suggestions, thank you!
It turns out that using multiple states was causing an issue, I'm assuming because it's asynchronous. So instead I used one state that held an object of states, like so:
const [states, setStates] = useState({
workoutName: "",
exercisesArr: [""],
weights: [[""]],
reps: [[""]],
restTimers: [""],
isDoneArr: [[false]],
originalWorkoutName: "",
});
The data was loaded as such:
const loadWorkoutData = async () => {
try {
console.log("loading workoutscreen data for:", route.params.name);
const unparsedWorkoutData = await AsyncStorage.getItem(route.params.name);
if (unparsedWorkoutData !== null) {
// We have data!
const workoutData = JSON.parse(unparsedWorkoutData);
setStates({
workoutName: route.params.name,
exercisesArr: workoutData[0],
weights: workoutData[1],
reps: workoutData[2],
restTimers: workoutData[3],
isDoneArr: workoutData[4],
originalWorkoutName: route.params.name,
});
}
} catch (error) {
// Error retrieving data
console.log("ERROR LOADING DATA:", error);
}
};

How to avoid firing multiple redux actions with real time firestore listeners

Introduction
A Little Warning: I do use Redux Toolkit
I have bunch of lists, one of which should be active. And depending on some context, active list should be different. For example I have 3 lists (A, B, C) and let's look at following patterns:
List B is active and I decided to create a new list. After creating list D, list D should be active:
List D - active
List C
List B
List A
List B is active and I decided to change the page. When I come back, List B should be active as it was before changing the page.
The problem
As I initiate the setListsAction from the beginning, it always listens to the firestore and gets invoked every time I manipulate with the store (add, remove, update) and then pass all the data to the reducer. For this reason, I can't control which action was actually performed. For this case in my setListsReducer I check if there's already an active list, if so, I don't change it (covering my second pattern in the examples section). However, with such logic I can't set newly created list as active, because there'll be always an active list that's why in my createListAction I pas a newly created list to the payload and in createListReducer I set payload as the active list. However, the caveat of this approach is that both setListsAction and createListAction gets triggered, so redux state gets updated two times in a row, making my components rerender unnecessary. The cycle looks like that:
in createListAction I add list to the firestore
firestore was updated, so setListsAction gets triggered
createListAction dispatches fulfilled action.
My Code
Actions
setListsAction
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = await db.collection('lists');
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
const lists = querySnapshot.docs.map((doc) => {
const list = { ...doc.data(), id: doc.id };
return list;
});
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
});
};
};
createListAction
export const createListActionAsync = (list) => {
return async (dispatch: Dispatch<PayloadAction<any>>) => {
dispatch(listsLoadingActions.pending());
const docList = await db.collection('lists').add(list);
const fbList = { ...list, id: docList.id };
dispatch(listsLoadingActions.fulfilled(fbList));
};
};
Reducers
setListsReducer
builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {
state.lists = payload.lists;
const activeList = state.activeList
? payload.lists.find((l) => l.id === state.activeList.id)
: payload.lists[0];
state.activeList = activeList;
});
createListReducer
builder.addCase(listsLoadingActions.fulfilled, (state, { payload }) => {
state.activeList = payload;
});
Sum
So I would like you to propose a better way to handle my problem. I tried to solve it, using change type on docChanges but when I init setListsAction, all docs' changes are type of added and workarounds may damage further implementations of the app. Probably, I need to give up real time database and use get method instead.
If you eliminate the createListReducer and listLoadingActions, you should be able to do everything from inside the ListsAction hook. Using await db.collection('lists').add(list) should refire the listener on the lists collection once it's been added to the database successfully.
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = db.collection('lists'); // no need to await?
let firstLoad = true; // used to determine whether to use docs or docsChanges
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
if (firstLoad) {
const lists = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
firstLoad = false;
// Get and set initial active list?
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
} else {
// optionally fire dispatch(fetchLoadingActions.pending()) again?
const listsCopy = [...state.lists]; // copy the existing list to mutate it
let activeList = state.activeList; // store the current activeList
querySnapshot.docChanges().map((change) => {
if (change.type === "added") {
const thisList = { ...change.doc.data(), id: change.doc.id };
listsCopy.splice(change.newIndex, 0, thisList);
activeList = thisList;
} else if (change.type === "modified") {
listsCopy.splice(change.oldIndex, 1);
listsCopy.splice(change.newIndex, 0, { ...change.doc.data(), id: change.doc.id });
} else if (change.type === "removed") {
listsCopy.splice(change.oldIndex, 1);
if (activeList.id === change.doc.id) {
// the current active list was removed!
activeList = undefined;
}
}
});
dispatch(
fetchLoadingActions.fulfilled({
lists: listsCopy,
activeList: activeList || listsCopy[0] // use activeList or fallback to first list in listsCopy, could still be undefined if listsCopy is empty!
})
);
}
});
return unsubscribe;
};
};
Regarding the active list history, you could either use the URL ?list=some-id to store the selected list with the History API or you could store an array called activeListHistory in your state variable where you push() and pop() to it as necessary (make sure to handle cases where the old list no longer exists and where there are no entries in the array).

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

React Redux await until first action is complete

I have an action that will take quite some time to complete. I want to inform the users that the action is currently taking place. To do this I am dispatching an action that updates a reducers state with a boolean.
The issue I am running into is that the larger action starts before the the state is updated. As a result, when a users clicks the generate button, they then experience the system lagging appose to UI informing them to wait.
How can I ensure the first action startGeneration() is completed before buildKeyMap() starts? Same questioned for reportProgress(). I have redux-thunk installed.
export const generateKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
dispatch(startGeneration()), // <-- this updates reducers state with a boolean, generatingMap: true
dispatch(buildKeyMap(mapFrom, mapTo)) // <-- this takes a long time to finish
};
};
export const buildKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
let keyMap = {};
for (let i = 0; i < mapFrom.length; i++) {
dispatch(reportProgress(i)); // <-- would like this to finish before moving on in the loop
let randomIndex = randomIntFromInterval(0, mapTo.length - 1);
let key = mapFrom[i].toString();
let value = mapTo[randomIndex].toString();
Object.assign(keyMap, { [key]: value });
mapTo.splice(randomIndex, 1);
}
dispatch(stopGeneration()); // <--- Would like this to finish before delivering the map
return { type: actionTypes.DELIVERKEYMAP, payload: keyMap };
};
};
export const startGeneration = () => {
return { type: actionTypes.STARTKEYMAPGENERATION };
};

update store on mobx react hook asynchronously

I am trying to fetch the data asynchronously from firestore to my local state using react hooks and mobx and got no clue how to update the store after getting data from firestore. I have used https://github.com/IjzerenHein/firestorter/blob/master/docs/API.md#Collection+query
My store
const store = useObservable({
forms: [],
async initForms(user_id) {
console.log(user_id);
my_forms.query = ref =>
ref
.where('userId', '==', user_id)
.limit(20);
let obj = {};
const arr = [];
my_forms.docs.forEach((doc, i) => {
obj = {
key: i + 1,
title: doc.data.title,
id: doc.id,
tags: doc.data.tags,
category: doc.data.category,
locked: doc.data.locked
};
arr.push(obj);
});
//i can see records from firestore
console.log('arr',arr);
return arr;
},
})
How i tried to update store
useEffect(() => autorun(() => {
store.forms=store.initForms(user_id);
}),[]);
store.forms && (
store.forms.length > 0 ? (
<FuseAnimateGroup
enter={{
animation: "transition.slideUpBigIn"
}}
className="flex flex-wrap py-24"
>
{store.forms.map((f) => {
Well i fixed it by using classic mobx store and using them as the context
These are the steps i did
Create the classic mobx store like we used to do in normal react project
export class MyFormsStore {
forms: []
async initForms(user_id) {
console.log(user_id);
my_forms.query = ref =>
ref
.where('userId', '==', user_id)
.limit(20);
await my_forms.fetch();
let obj = {};
const arr = [];
my_forms.docs.forEach((doc, i) => {
obj = {
key: i + 1,
title: doc.data.title,
id: doc.id,
tags: doc.data.tags,
category: doc.data.category,
locked: doc.data.locked
};
arr.push(obj);
});
this.forms = arr;
}
}
decorate(MyFormsStore, {
forms: observable
})
Export it as the context
export default createContext(new MyFormsStore());
Play with store as the functional component using useContext hook
const Forms = observer((props) => {
const store = useContext(MyFormsStore)
.
.
.
Fetch the data from firestore using useEffect hook
useEffect(() => {
store.initForms(user_id);
}, []);
Check if the store has been initialised or not , it yes render it
{
store.forms !== undefined && (
store.forms.length > 0 ? (
//render
I am still looking how can I memoize expensive functions so that I can avoid calling them on every render using useMemo . Feedbacks will be appreciated
You don't need the useEffect to update data that is tracked by Mobx.
The simplest way is to use runInAction from MobX.
So when your async function is finally resolved, you then use runInAction to update the store. And that will eventually trigger the rendering of react components that are using that particular data.
something like this:
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// after await, modifying state again, needs an actions:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
You can read more in official documentation:
Writing asynchronous actions

Resources