React Native fetch before render components - reactjs

I am creating a Reat Native app which connects to an API from which it gets data.
I am using React Navigation to handle navigation. The app has a Stack Navigator and a Bottom Tab Navigator. The StackNavigator has 4 screens:
SignupScreen which handles creating account;
LoginScreen for handlong log in;
SplashScreen that checks for a local token and logs in the user automatically;
A LoadingScreen that triggers the initial fetch call to the API, stores the response in state and navigates to the MainFlow screen;
A MainFlow screen that contains the TabNavigator.
The TabNavigator has two screens, FeedScreen, Account and More where the initial screen is FeedScreen.
The signup/login/local flows are all working fine.
The issue: Once the user is logged in successfully the LoadingScreen is triggering the API call but the MainFlow components are being rendered before the data is in state. Because the components in MainFlow need the data, an error is thrown. How can I render the FeedScreen components only once the data is there?
In the LoadingScreen I am triggering an API call on useEffect from a context object, QuestionContext:
const LoadingScreen = ({ navigation }) => {
const [loading, setLoading] = useState(true);
const { state: authState } = useContext(AuthContext);
const { getQuestionsForUser, getAllQuestions } = useContext(QuestionContext);
useEffect(() => {
getAllQuestions();
}, []);
return (
<View style={styles.container}>
<YonStatusBar backgroundColor="#310B3B" />
<Image source={splashLogo} containerStyle={styles.splashLogo} />
<ActivityIndicator />
</View>
);
};
export default LoadingScreen;
getAllQuestions is a function in QuestionContext which makes the API call and navigates to FeedScreen:
const getAllQuestions = (dispatch) => {
return async () => {
try {
const token = await AsyncStorage.getItem('token');
const config = { headers: { Authorization: `Bearer ${token}` } };
const response = await yonyonApi.get(`/questions`, config);
dispatch({ type: 'GET_ALL_QUESTIONS', payload: response.data });
RootNavigation.navigate('MainFlow');
} catch (e) {
console.log(e);
}
};
};
getAllQuestions is working fine: the API call is successful and I can see that the response is stored in state. However, it navigates to MainFlow before that happens.
Finally, this is the FeedScreen:
const FeedScreen = () => {
const { state: questionState } = useContext(QuestionContext);
return (
<ScrollView style={styles.container}>
{console.log(questionState.questions)}
<View style={styles.listContainer}>
<QuestionCard />
</View>
</ScrollView>
);
};
export default FeedScreen;
The FeedScreen renders a QuestionCard which needs the data in questionState. This is what throwing the error: the QuestionCard is being rendered before the data is in state.
How can I make the navigation only navigate to FeedScreen once the necessary data is in state? Or alternatively, render something else than the QuestionCard while the data is not there and once the data is in questionState render the QuestionCard?

For me i will use screen instead of two screens as follows :
const FeedScreen = () => {
const [loading, setLoading] = useState(true);
const { state: authState } = useContext(AuthContext);
const [data, setData] = useState([]);
const getAllQuestions = (dispatch) => {
return async () => {
try {
const token = await AsyncStorage.getItem('token');
const config = { headers: { Authorization: `Bearer ${token}` } };
const response = await yonyonApi.get(`/questions`, config);
setData(response.data)
setLoading(false)
} catch (e) {
console.log(e);
}
};
};
useEffect(() => {
getAllQuestions();
}, []);
return (
<ScrollView style={styles.container}>
{
(loading)?
<ActivityIndicator/>
:
<View style={styles.listContainer}>
<QuestionCard data={data}/>
</View>
}
</ScrollView>
);
};
export default FeedScreen;

Why don't you set the initial state of your context to null and render your component if it is not null ?
const [questionState, setQuestionState] = useState(null);
...
const FeedScreen = () => {
const { state: questionState } = useContext(QuestionContext);
return (
<ScrollView style={styles.container}>
{!!questionState?.questions && console.log(questionState.questions)}
<View style={styles.listContainer}>
<QuestionCard />
</View>
</ScrollView>
);
};
export default FeedScreen;

Related

React Native Firebase refresh user record

I'm currently building a firebase login system with a verified email screen.
My problem is that I have a reload user button on the verified email screen that updates the user's credentials so that my root directory redirects the user to the AppStack if currentUser.emailVerified === true.
but the reload button isn't being triggered once pressed so that my root directory is still currentUser.emailVerified === false and not redirecting the user to the AppStack.
Login-System/context/AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from 'react';
import { auth } from '../config';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function sendVerification() {
return currentUser.sendEmailVerification();
}
const getUser = () => auth.currentUser;
const reloadUser = () => getUser().reload();
const reload = async () => {
try {
await reloadUser();
const user = getUser();
setCurrentUser(user);
} catch (error) {}
return reload;
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
const value = {
currentUser,
loading,
reload,
sendVerification,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
Login-System/screens/VerifyEmailScreen.js:
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useAuth } from '../contexts/AuthContext';
import { View, Button } from '../components';
import { Colors } from '../config';
export const VerifyEmailScreen = () => {
const { currentUser, reload, sendVerification } = useAuth();
const handleSendVerification = async () => {
try {
await sendVerification();
} catch (error) {}
return handleSendVerification;
};
return (
<>
<View isSafe style={styles.container}>
<View style={styles.center}>
<Text style={styles.screenTitle}>Check your email</Text>
<Text style={styles.screenInfo}>{currentUser.email}</Text>
<Text style={styles.screenInfo}>
We sent you an email with instructions on how to verify your email
address. Click on the link in the email to get started.
</Text>
<Button
style={styles.button}
onPress={() => handleSendVerification()}>
<Text style={styles.buttonText}>Resend</Text>
</Button>
<Button style={styles.button} onPress={reload}>
<Text style={styles.buttonText}>Done</Text>
</Button>
</View>
</View>
</>
);
};
I did some similar works to check if the user email is verified or not you can use this function:
export function getUserEmailVerified() {
const user = firebase.auth().currentUser;
return !!user ? (user.emailVerified ? 'Yes' : 'No') : 'No';
}
To trigger an email verification you can use this method. This method will trigger an email verification and refresh the user.
export function verificationEmail(email, onSuccess) {
refreshUser();
const user = firebase.auth().currentUser;
const finishAction = message => {
onSuccess();
showToast(message);
};
user
.sendEmailVerification()
.then(() => {
refreshUser();
})
.catch(error => finishAction(error.message));
}
And to refresh the user you can use this method.
export function refreshUser() {
let user = firebase.auth().currentUser;
if (!!user && !user?.emailVerified) {
interval = setInterval(() => {
user?.reload().then();
}, 3000);
}
}
You also need to use onUserChanged from firebase to detect whether user information is changed or not. It returns a listener if your user email verified field is changed anyway it will get here is an example.
export function onUserChanged(listener) {
firebase.auth().onUserChanged(listener);
}

Cannot get datas on json with flatlist

I am using the Free Meal API with flatlist. I have Category component, Categories page, useFetch hook. I can't see Flatlist on screen. I can get console log of data but I can't reach datas with flatlist.
.env folder:
API_URL_CATEGORIES="https://www.themealdb.com/api/json/v1/1/categories.php"
API_URL_FILTER="https://www.themealdb.com/api/json/v1/1/filter.php?"
useFetch hook for getting the data in URL and returning Loading icon, Error if URL doesn't work and data for data in URL.
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const fetchData = async () => {
try {
const {data: responseData} = await axios.get(url);
setData(responseData);
setLoading(false); }
catch (error) {
setError(error.message);
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return {error, loading, data};
};
export default useFetch;
Category component:
const Category= ({category, onSelect}) => {
return(
<TouchableOpacity style={styles.container} onPress={onSelect}>
<Image
style={styles.image}
source={{uri:category.strCategoryThumb}} />
<Text style={styles.title}>{category.strCategory}</Text>
</TouchableOpacity>
)
}
export default Category;
Categories page:
const Categories = ({navigation}) => {
const { error, loading, data } = useFetch(config.API_URL_CATEGORIES);
console.log(data)
const handleCategorySelect = strCategory => {
navigation.navigate("Detail", {strCategory})
}
const renderCategory = ({item}) => <Category category={item} onSelect={() => handleCategorySelect(item.strCategory)}/>;
if(loading) {
return <Loading/>;
}
if(error) {
return <Error/>;
}
return(
<View style={styles.container}>
<FlatList data={data} renderItem={renderCategory}/>
<Text>Categorises</Text>
</View>
)
}
export default Categories;
I think data is actually object that contains a property categories , which holds an array.
try data.categories and I believe this should work fine.

How to implement infinite list using React Query in react native with Flatlist

Here is my code:
import React, {useState} from 'react';
import {useQuery, useInfiniteQuery} from 'react-query';
import {getMeetup} from '../../api/methods/getMeetups';
export default function MyFunction(props) {
const [next, setNext] = useState('');
const fetchData = async ({pageParam = ''}) => {
const response = await getMeetup( pageParam);
console.log('API RESP', response);
return response;
};
const {data, isLoading, fetchNextPage} = useInfiniteQuery(
'myData',
fetchData,
{
getNextPageParam: (lastPage, pages) => lastPage?.next?._id,
},
);
console.log('RQUERY CHECK', data);
const getMore = () => {
console.log('data end', data?.pages[0]?.next?._id);
fetchNextPage({pageParam: data?.pages[0]?.next?._id});
};
const flattenData = data?.pages
? data?.pages?.flatMap((page) => [...page.Docs])
: [];
return (
<View>
<FlatList
style={{
marginBottom: verticalScale(40),
paddingHorizontal: scale(15),
}}
data={flattenData}
keyExtractor={(item) => item._id}
renderItem={({item, index}) => {
return <ListItem data={item} index={index} />;
}}
onEndReachedThreshold={0.1}
onEndReached={getMore}
/>
</View>
);
}
The problem i am facing is when the page loads the api calls one by one with unique next ids or page param. What i was trying to implement is , when user reaches the end of the page (onEndReached) the data needs to be fetched with a new page param.
getNextPageParam return the value for the next page. So you don't need pass pageParam in fetchNextPage unless you want overwrite, for any reason, the next page value.
You can add a hasNextPage validation for unnecessary requests.
const {data, isLoading, hasNextPage, fetchNextPage} = useInfiniteQuery( /* your code */ )
const getMore = () => {
if(hasNextPage)
fetchNextPage();
};

Getting response from redux function

I'm doing a react native app using redux.
I'll like to post some data to create a new document on database, which actionally works! But how I get the result of the function so I can check if there is any error?
Bellow involved lines of my code:
on the view:
handleCreatePostPress() {
const { createPost } = this.props;
const testPost =
{
"title": "TEST FROM APP",
"description": "My description",
}
console.log(JSON.stringify(this.state));
createPost(testPost);
}
....
<View style={{flex:1}}>
<View style={{justifyContent: 'center', alignItems: 'center', padding:20}}>
<Button style={{color:'#e0e0e0'}} block onPress={() => this.handleCreatePostPress()}>
<Text style={{color:'#ffffff', padding:10}}>CREATE POST</Text>
</Button>
</View>
</View>
.....
const mapStateToProps = (state) => {
return {
};
};
const mapDispatchToProps = {
createPost: createPostAction,
};
export default connect(mapStateToProps, mapDispatchToProps) (createPostScreen);
and my redux action code....
export const createPostSuccess = createAction('CREATE_POST_SUCCESS')
export const createPost = (data) => async (dispatch) => {
try {
const response = await api.posts.create(data);
dispatch(createPostSuccess(response.data));
} catch (error) {
dispatch(handleAPIError(error));
}
};
so how can I check here if there is an error:
createRaid(testRaid); // how to check if there is error, if has finished?
Thank you very much this is my first app on react native....
You cant return a result from a dispatch function.
What you can do instead is to process your data inside createPost.
If valid, dispatch as per normal,
if not, dispatch an error message.
This error message should be captured in your redux reducer, which the component will track and display errors accordingly.
import { useSelector } from 'react-redux';
export let myComponent = () => {
const errorMsg = useSelector(store => store.myReducer.errorMsg);
return (
<View>
<Button> {...}</Button>
<Text>{errorMsg}</Text>
</View>
)
}

TypeError: undefined is not a function (near "...dataSource.map...")

I am trying to render the API data onto the page but get this error
TypeError: undefined is not a function (near "...dataSource.map...")
Unrelated Question: Also I am new to mobile app development and am wondering when you get data from an API is best practice to store it in a database or render it directly onto a page?
import React, { Component, useState, useEffect } from "react";
import { View, Text, StyleSheet, ActivityIndicator } from "react-native";
import { ScreenContainer } from "react-native-screens";
export const Home = () => {
const [isLoading, setisLoading] = useState(true);
const [dataSource, setdataSource] = useState(null);
useEffect(async () => {
const response = await fetch(
"https://facebook.github.io/react-native/movies.json"
);
const data = await response.json();
const movies = data.title;
setdataSource(movies);
setisLoading(false);
// .catch((error)) => {
// console.log(error)
// }
}, []);
if (isLoading) {
return (
<View>
<ActivityIndicator />
</View>
);
} else {
let moviest = dataSource.map((val, key) => {
return (
<View key={key}>
<Text>{val}</Text>
</View>
);
});
return (
<ScreenContainer style={styles.container}>
<View>{moviest}</View>
</ScreenContainer>
);
}
You have to check dataSource is null or undefined before map.
let moviest = dataSource && dataSource.map((val, key) => {
Please check type of dataSource.
When I check https://facebook.github.io/react-native/movies.json data, type of data.title is string("The Basics - Networking")
Maybe you should use movies.
const data = await response.json();
const movies = data.movies;
setdataSource(movies);
setisLoading(false);

Resources