State is undefined, but ONLY in Test Flight - javascript

I have the following component with the following functions (alerts included to show where state is weird):
const CategoryArticlesChild = (props) => {
const categories = useSelector((state) => state.settings.cats);
const show_read = useSelector((state) => state.settings.show_read);
const skills = useSelector((state) => state.settings.skill);
const date = useSelector((state) => state.date);
...
const loadArticlesByCategory = () => {
const hsks = getLevels(skills);
const limit = (categories.length + 1) * 15;
let _query = {
category: categories,
limit: limit,
ordering: "-date",
published: 1,
display_skill: hsks,
taxon: props.taxon,
show_read: show_read,
};
alert(JSON.stringify(_query));
alert(JSON.stringify(date));
_query = processDates(date, _query);
if (!_.isEqual(query, _query)) {
setQuery(_query);
} else {
setIsLoading(false);
}
};
useEffect(() => {
if (!isLoading) {
setIsLoading(true);
}
if (
categories &&
skills &&
!_.isUndefined(date) &&
date &&
show_read !== null
) {
loadAllArticles();
}
}, [categories, date, skills, show_read]);
...
}
The ONLY place that loadArticlesByCategory is called is in the useEffect above.
And on web (I am using React Native Web), iOS simulator and Xcode build on device, it all works fine.
But then when I push to Test Flight, date is undefined. Even though, as you can see, I use a Lodash function to check DIRECTLY if it is undefined, and prevent execution of the function if it isn't defined.
What exactly am I missing here? Why is date constantly undefined, but ONLY in Test Flight, and despite me setting code that should explicitly stop it from being undefined

Changing the code to the following (and also added the arguments to the other function):
useEffect(() => {
if (!isLoading) {
setIsLoading(true);
}
if (
categories &&
skills &&
!_.isUndefined(date) &&
date &&
show_read !== null
) {
loadArticlesByCategory(categories, skills, date, show_read);
}
}, [categories, date, skills, show_read]);
Fixed this particular bug.
So my best guess is that this is some sort of React immutability issue or some sort of race condition?

Related

UseQuery graphql -> useState with useEffect such that I can run second useEffect to set more state

I am using Apollo for graphQL queries in a NextJS project I have the following request. I feel like the solution to this is simple, but the error occurs at the stateData.allProducts section it is saying it is null, but I have set the state in the useEffect and it has data as a dependency in the array, so shouldn't it re-render once data has loaded.
This is all works just fine if I placed these checks below a if statement checking the loading and return ...loading but then I can't use useEffect.
Any help as to what I am doing wrong would be greatly appreciated.
Thanks!
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
// Creating state and setting it from query
const [stateData, setStateData] = useState(null);
const [disableAddToCart, setDisableAddToCart] = useState(false);
useEffect(() => {
setStateData(data);
}, [data]);
//~~~~~~~// <--- RIGHT HERE IS WHERE I GET THE NULL ERROR
const productFound = stateData.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
const currentItem = cartDetails[productFound.id];
useEffect((): void => {
console.log("currentItem", currentItem);
if (currentItem) {
if (currentItem.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}
}, [currentItem]);
As long as your query is loading, or if there is an error, the data variable from useQuery will be null.
Therefore you have to check for the loading to have finished and that no error has occurred. And/or for the data to be defined.
Also, stateData is unnecessary, because data is already a ready-to-use state variable.
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
const [disableAddToCart, setDisableAddToCart] = useState(false);
let productFound;
let currentItem;
if(!loading && !error && data?.allProduct){
productFound = data.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
currentItem = cartDetails[productFound.id];
}
useEffect((): void => {
//since you are using typescript, you can use the optional chaining operator
if (currentItem?.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}, [currentItem]);

How to put a dynamic data from firestore in the function where() and also use the snap.size to count the total query to be passed in a graph?

I have this data from firestore and I wanted to retrieve it dynamically with a where() but this is the error I'm getting:
TypeError: vaccines is not a function
The user collection:
[![enter image description here][1]][1]
Below are the codes:
const Vaccine = () => {
const [vaccines, setVaccines] = useState([]);
useEffect(() => {
const unsubscribe = firestore
.collection("vaccines")
.onSnapshot((snapshot) => {
const arr = [];
snapshot.forEach((doc) =>
arr.push({
...doc.data(),
id: doc.id,
})
);
setVaccines(arr);
});
return () => {
unsubscribe();
};
}, []);
Preface
As highlighted in the comments on the original question, this query structure is not advised as it requires read access to sensitive user data under /users that includes private medical data.
DO NOT USE THIS CODE IN A PRODUCTION/COMMERICAL ENVIRONMENT. Failure to heed this warning will lead to someone suing you for breaches of privacy regulations.
It is only suitable for a school project (although I would a fail a student for such a security hole) or proof of concept using mocked data. The code included below is provided for education purposes, to solve your specific query and to show strategies of handling dynamic queries in React.
From a performance standpoint, in the worst case scenario (a cache miss), you will be billed one read, for every user with at least one dose of any vaccine, on every refresh, for every viewing user. Even though your code doesn't use the contents of any user document, your code must download all of this data too because the Client SDKs do not support the select() operator.
For better security and performance, perform this logic server-side (e.g. Cloud Function, a script on your own computer, etc) and save the results to a single document that can be reused by all users. This will allow you to properly tighten access to /users. It also significantly simplifies the code you need to display the graphs and live statistics on the client-side.
useEffect
As stated by the React documentation on the Rules of hooks:
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
The documentation further elaborates that React relies on the order in which Hooks are called, which means that you can't have hook definitions behind conditional logic where their order and quantity changes between renders. If your hooks rely on some conditional logic, it must be defined inside of the hook's declaration.
As an example, if you have an effect that relies on other data, with this logic:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
if (userProfile) {
useEffect(() => {
// get user post list and store in userPosts
}, [userProfile]);
}
you need to instead use:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
useEffect(() => {
if (!userProfile) {
// not ready yet/signed out
setUserPosts(null);
return;
}
// get user post list and store in userPosts
}, [userProfile]);
Similarly, for arrays:
someArray && someArray.forEach((entry) => {
useEffect(() => {
// do something with entry to define the effect
}, /* variable change hooks */);
});
should instead be:
useEffect(() => {
if (!someArray) {
// not ready yet
return;
}
const cleanupFunctions = [];
someArray.forEach((entry) => {
// do something with entry to define an effect
cleanupFunctions.push(() => {
// clean up the effect
});
});
// return function to cleanup the effects created here
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
}
}, /* variable change hooks */);
Because this looks a lot like lifecycle management, you are actually better off replacing it with nested components rather than using hooks, like so:
return (
<> // tip: React.Fragment shorthand (used for multiple top-level elements)
{
someArray && someArray
.map(entry => {
return <Entry key={entry.key} data={entry.data} />
})
}
</>
);
Adapting to your code
Note: The code here doesn't use onSnapshot for the statistics because it would cause a rerender every time a new user is added to the database.
const getVaccineStats = (vaccineName) => {
const baseQuery = firestore
.collection("users")
.where("doses.selectedVaccine", "==", vaccine);
const oneDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.get()
.then(querySnapshot => querySnapshot.size);
const twoDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.get()
.then(querySnapshot => querySnapshot.size);
return Promise.all([oneDoseQueryPromise, twoDoseQueryPromise])
.then(([oneDoseCount, twoDoseCount]) => ({ // tip: used "destructuring syntax" instead of `results[0]` and `results[1]`
withOneDose: oneDoseCount,
withTwoDoses: twoDoseCount
}));
};
const Vaccine = () => {
const [vaccines, setVaccines] = useState();
const [vaccineStatsArr, setVaccineStatsArr] = useState([]);
// Purpose: Collect vaccine definitions and store in `vaccines`
useEffect(() => {
return firestore // tip: you can return the unsubscribe function from `onSnapshot` directly
.collection("vaccines")
.onSnapshot({ // tip: using the Observer-like syntax, allows you to handle errors
next: (querySnapshot) => {
const vaccineData = []; // tip: renamed `arr` to indicate what the data contains
querySnapshot.forEach((doc) =>
vaccineData.push({
...doc.data(),
id: doc.id,
});
);
setVaccines(vaccineData);
}),
error: (err) => {
// TODO: Handle database errors (e.g. no permission, no connection)
}
});
}, []);
// Purpose: For each vaccine definition, fetch relevant statistics
// and store in `vaccineStatsArr`
useEffect(() => {
if (!vaccines || vaccines.length === 0) {
return; // no definitions ready, exit early
}
const getVaccineStatsPromises = vaccines
.map(({ vaccine }) => [vaccine, getVaccineStats(vaccine)]);
// tip: used "destructuring syntax" on above line
// (same as `.map(vaccineInfo => [vaccineInfo.vaccine, getVaccineStats(vaccineInfo.vaccine)]);`)
let unsubscribed = false;
Promise.all(getVaccineStatsPromises)
.then(newVaccineStatsArr => {
if (unsubscribed) return; // unsubscribed? do nothing
setVaccineStatsArr(newVaccineStatsArr);
})
.catch(err => {
if (unsubscribed) return; // unsubscribed? do nothing
// TODO: handle errors
});
return () => unsubscribed = true;
}, [vaccines]);
if (!vaccines) // not ready? hide element
return null;
if (vaccines.length === 0) // no vaccines found? show error
return (<span class="error">No vaccines found in database</span>);
if (vaccineStatsArr.length === 0) // no stats yet? show loading message
return (<span>Loading statistics...</span>);
return (<> // tip: React.Fragment shorthand
{
vaccineStatsArr.map(([name, stats]) => {
// this is an example component, find something suitable
// the `key` property is required
return (<BarGraph
key={name}
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>);
});
}
</>);
};
export default Vaccine;
Live Statistics
If you want your graphs to be updated live, you need "zip together" the two snapshot listeners into one, similar to the rxjs combineLatest operator. Here is an example implementation of this:
const onVaccineStatsSnapshot => (vaccine, observerOrSnapshotCallback, errorCallback = undefined) => {
const observer = typeof observerOrCallback === 'function'
? { next: observerOrSnapshotCallback, error: errorCallback }
: observerOrSnapshotCallback;
let latestWithOneDose,
latestWithTwoDoses,
oneDoseReady = false,
twoDosesReady = false;
const fireNext = () => {
// don't actually fire event until both counts have come in
if (oneDoseReady && twoDosesReady) {
observer.next({
withOneDose: latestWithOneDose,
withTwoDoses: latestWithTwoDoses
});
}
};
const fireError = observer.error || (err) => console.error(err);
const oneDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.onSnapshot({
next: (querySnapshot) => {
latestWithOneDose = querySnapshot.size;
oneDoseReady = true;
fireNext();
},
error: fireError
});
const twoDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.onSnapshot({
next: (querySnapshot) => {
latestWithTwoDoses = querySnapshot.size;
twoDosesReady = true;
fireNext();
},
error: fireError
});
return () => {
oneDoseUnsubscribe();
twoDoseUnsubscribe();
};
}
You could rewrite the above function to make use of useState, but this would unnecessarily cause components to rerender when they don't need to.
Usage (direct):
const unsubscribe = onVaccineStatsSnapshot(vaccineName, {
next: (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
},
error: (err) => {
// TODO: error handling
}
);
or
const unsubscribe = onVaccineStatsSnapshot(vaccineName, (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
});
Usage (as a component):
const VaccineStatsGraph = (vaccineName) => {
const [stats, setStats] = useState(null);
useEffect(() => onVaccineStatsSnapshot(vaccineName, {
next: (newStats) => setStats(newStats),
error: (err) => {
// TODO: Handle errors
}
}, [vaccineName]);
if (!stats)
return (<span>Loading graph for {vaccineName}...</span>);
return (
<BarGraph
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>
);
}
vaccines is an array and not a function. You are trying to run a map on vaccines. Try refactoring your code to this:
vaccines &&
vaccines.map((v, index) => {
// ...
})
Also do check: How to call an async function inside a UseEffect() in React?
here is the code, that works for you:
function DatafromFB() {
const[users, setUsers] = useState({});
useEffect(()=>{
const fetchVaccine = async () => {
try {
const docs = await db.collection("vaccines").get();;
docs.forEach((doc) => {
doc.data().vaccineDetails
.forEach(vaccineData=>{
fetchUsers(vaccineData.vaccine)
})
})
} catch (error) {
console.log("error", error);
}
}
const fetchUsers = async (vaccine)=>{
try {
const docs = await db.collection("users")
.where("doses.selectedVaccine", "==", vaccine).get();
docs.forEach(doc=>{
console.log(doc.data())
setUsers(doc.data());
})
}catch(error){
console.log("error", error);
}
}
fetchVaccine();
},[])
return (
<div>
<h1>{users?.doses?.selectedVaccine}</h1>
</div>
)
}
export default DatafromFB
what is ${index.vaccine} I think it must be v.vaccine
also setSize(snap.size); will set set size commonly not vaccine specific

React - After typing more than a character in search bar, it stops fetching users

Case: Search bar retrieves users just fine based on ONE character, once you type a second character, it retrieves nothing anymore.
Example if you wanna look for Andrew, and you type "A", it will retrieve all users that start with A, if you type your second letter, in this case N, it will retrieve nothing anymore.
This is quite a big project I'm working on, long code coming in:
My search bar:
Import Select from 'react-select';
<Select
onInputChange={handleInputChange}
onChange={userHandler}
className="searchPlayer"
options={options}
placeholder={t('searchBar', {results: totalResults})}
search={true}
/>
handleInputChange:
const handleInputChange = search => {
if (search) {
const newFilters = {
...filters,
search: search,
};
setFilters(newFilters);
}
};
setFilters:
const [filters, setFilters] = useState({});
userHandler (if relevant):
const selectSearchUserHandler = (data) => {
history.push(`/accounts/${data.id}`);
};
My useEffect to fetch new accounts:
useEffect(() => {
if (status !== 'idle') {
dispatch(fetchAccounts({ filters, options }));
}}, [filters, options]);
options -> nothing more than an object with the page the user is on + results limit per page. { page: 1, limit: 15 }
My action for fetching new accounts:
export const fetchAccounts = createAsyncThunk(
'accounts/fetchAccounts',
async ({ filters, options }) => {
const filterParams = new URLSearchParams(
Object.assign(filters, options),
).toString();
const {
data: { data },
} = await get(`/accounts?${filterParams}`);
return data;
},
);
I think it has to do with the fact that this callback contains a copy of filters and it's never getting refreshed:
const handleInputChange = search => {
if (search) {
const newFilters = {
...filters,
search: search,
};
setFilters(newFilters);
}
};
Make sure that you are retrieving a current value of filters in the event handler.
Adding filterOption={false} to my react-select element fixed it for me.

How to redirect to another page if user not in that team in nextjs

I am trying to create a function that make user who try to get into team page when they are not in that team.
Here is my following code:
const [user, setUser] = useState({});
const fetchCurrentUser = () => {
axios
.get(`/api/v1/profiles/profile/${serverUserData.publicId}`)
.then((res) => {
setUser(res.data);
if (
res.data.Teams.length <= 0
// ||
// res.data.Teams.map((team) => team.public_team_id !== teamProfileId)
) {
router.push("/profile");
}
})
.catch((err) => {
throw new Error(err);
});
};
So I am trying to achieve res.data.Teams.map((team) => team.public_team_id !== teamProfileId
example: if that user have 3-4 teams but not this team so they cant get into this team page
All you have to do is check if the Teams has a team matching teamProfileId then let them access else not.
You can check with Array.some() method.
if (res.data.Teams.some(team => team.public_team_id !== teamProfileId)) {
router.push("/profile");
}
It will return true if the condition passes, i.e no matching teamProfileId; else returns false.

Loading state not effective in the React application (MobX)

I am coding a React app that fetches data from WordPress REST API. Everything works fine so far, however, loading indicator does not show up. I use MobX for state management. I have a loadingInitial observable. Whenever I start my action, I set this observable true to get into loading state. After the action does necessary operations, I reset loadingInitial to false. So I expect to see loading screen while fetching posts. But I see blank page while the daha is loading.
Here are the code for the action:
#action loadAnecdotes = async (page: number, year: number, order: string) => {
this.loadingInitial = true
try {
const anecdotesHeaders = year === 0 ? await agent.AnecdotesHeaders.list() : await agent.AnecdotesHeaders.listByYear(year)
const maxPages = anecdotesHeaders['x-wp-totalpages']
if (page <= maxPages) {
const anecdotes = year === 0 ? await agent.Anecdotes.list(page, order) : await agent.Anecdotes.listByYear(page, year, order)
runInAction(() => {
this.anecdoteArray = []
anecdotes.forEach((anecdote, i) => {
this.anecdoteArray[i] = anecdote
})
this.loadingInitial = false
})
} else {
runInAction(() => {
this.loadingInitial = false
})
}
return {anecdoteArray: this.anecdoteArray, maxPages}
} catch (error) {
console.log(error)
this.loadingInitial = false
}
}
Here is my useEffect in the component where I fetch the posts:
useEffect(() => {
loadAnecdotes(page, year, order).then(result => {
if (page <= result?.maxPages)
setArray(a => [...a, ...result?.anecdoteArray!])
setLoaded(true)
})
}, [loadAnecdotes, page, year, order, setLoaded, setArray])
Here is the what I call just before the returning the posts:
if (loadingInitial) return <LoadingComponent content='Anecdotes loading...' />
As a side note, my component is set as an observer.
What could I be doing wrong?
Well, it seems loadingInitial is not enough alone. I added a second condition to it, the length of the posts array. If it is 0, show the loading component. And it worked! Just this edit solves the problem:
if (loadingInitial || !array.length) return <LoadingComponent content='Anecdotes loading...' />

Categories

Resources