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
Related
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);
}
};
I ran into an asynchronous useState problem.
I have a situation where I first need to add an object to the state array in the handler. And then add this state to the localStorage.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
If I delete the entire localStorage first and run the handler. So an empty array is added to my localStorage (the state shows me updated). After the action again, the required object is finally added to my array.
I tried something like this, but still the same problem.
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
localStorage.setItem(storageItemName, JSON.stringify(tempArray));
How do you solve this, please?
/// EDIT
I have something like this
const FavoriteSong = () => {
const song = { id: 1, name: "Lala", length: "3:20" };
const [favoritedSongs, setFavoritedSongs] = useState([]);
const [isFavorited, setIsFavorited] = useState(false);
useEffect(() => {
if (localStorage.getItem("storageItemName")) {
const storageSongs = JSON.parse(
localStorage.getItem("storageItemName") || ""
);
setFavoritedSongs(storageSongs);
const foundSong = storageSongs?.find((song) => song.id === song.id);
foundSong ? setIsFavorited(true) : setIsFavorited(false);
}
}, [song]);
const handleClick = () => {
if (isFavorited) {
const filteredSong = favoritedSongs.filter(
(song) => song.id !== song.id
);
localStorage.setItem("storageItemName", JSON.stringify(filteredSong));
setIsFavorited(false);
} else {
setFavoritedSongs((prev) => [...prev, song]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
setIsFavorited(true);
}
};
return <div onClick={handleClick}>CLICK</div>;
};
export default FavoriteSong;
Just place your localStorage.set logic inside a useEffect to make sure it runs after the state actually changes.
useEffect() => {
localStorage.setItem(...);
}, [favoritedSongs]};
For that you can Use the condition If data in the array then It will set in localStorage otherwise not
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
tempArray.length && localStorage.setItem(storageItemName, JSON.stringify(tempArray));
.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
FavoritedSongs.length(your state name) && localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
(state) => state?.clientAddress?.getAddress
);
useEffect(() => {
dispatch(GET_CLIENT_ADDRESS({}))
}, [dispatch]);
const onSavedAddressSelect = (addressId) => {
debugger
if (addressId) {
const { data: selectedAddress = [] } = clientAddress ?? {} ;
console.clear()
console.log(selectedAddress)
const addressObj = selectedAddress?.filter(
(addressItem) => addressItem?.value === addressId
)?.[0];
setAddress(addressObj);
setSelectedVenue(addressObj?.value);
}
props.drawer.setDrawer({
open: false,
});
};
My clientAddress inside onSavedAddressSelect is not getting updated with latest redux state even if I dispatch on first render on useEffect.
Please help.
isn't clientAddress is not updated because you call
dispatch(GET_CLIENT_ADDRESS({})) with empty object, so it's stays the same? Try to pass there some object with values
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().
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;
}