React Component Not Re Rendering using Spread Operator - javascript

I've tried other solutions on this site, but I am not sure why this is not working. I have a response I am getting from a server each time a button is pressed. The response comes through fine and I am able to see it each time the button is pressed. I am receiving an array of objects from the server, and I am using the useState hook to set the state of a variable to keep track of the objects in the array. However, the component does not re render. Interestingly, if I add a console.log statement to see the contents of the state variable and then save the page, I can see that the state variable was updated properly. The component still does not re render though. The relevant code is here:
import React, { useEffect, useState } from "react";
import { StyleSheet, View, TouchableOpacity, Alert } from "react-native";
import tailwind from "tailwind-rn";
import colors from "../config/colors";
import useAuth from "../hooks/useAuth";
import Screen from "../components/Screen";
import AppText from "../components/AppText";
import { MaterialCommunityIcons } from "#expo/vector-icons";
import { getData } from "../hooks/useCache";
import { useIsFocused } from "#react-navigation/native";
import ListScreen from "./ListScreen";
const ProfileScreen = () => {
const { logout, user } = useAuth();
const [likes, setLikes] = useState("");
const [completed, setCompleted] = useState("");
const [responseJson, setResponseJson] = useState(null);
const isFocused = useIsFocused();
useEffect(() => {
const likesFunc = async() => {
setLikes(await getData("likes"));
setCompleted(await getData("eventsCompleted"));
try {
const response = await fetch(
"server url here",
{
headers: { Authorization: "Bearer " + user.idToken },
}
);
const responseJson = await response.json();
setLikes(responseJson.likes);
setCompleted(responseJson.eventsCompleted);
} catch {
Alert.alert("There has been an error processing your profile");
}
}
likesFunc();
}, []);
//get voted events
//run when questionsLeft is 0 to save num of calls
useEffect(() => {
const eventFunction = async() => {
try {
const response = await fetch(
"server url here",
{
headers: { Authorization: "Bearer " + user.idToken },
}
)
const res = await response.json();
setResponseJson([...res]);
} catch (error) {
Alert.alert(
"An error has occurred loading your questions. Close the app and try again."
);
console.log(error);
}
}
eventFunction();
}, [isFocused]);
return (
<Screen style={styles.bg}>
<View
style={[
tailwind("w-full flex-row py-4 justify-center items-center top-0"),
{ justifyContent: "space-between" },
]}
>
<AppText style={{ color: colors.white, fontSize: 30, marginLeft: 5 }}>
Hello, {user.displayName.split(" ")[0]}
</AppText>
<TouchableOpacity
style={styles.logoutButton}
onPress={() => {
Alert.alert("Log Out", "Are you sure you want to log out?", [
{
text: "Yes",
style: "destructive",
onPress: logout,
},
{
text: "Cancel",
style: "cancel",
},
]);
}}
>
<MaterialCommunityIcons
name="logout-variant"
size={25}
color={colors.primary}
/>
</TouchableOpacity>
</View>
<View
style={tailwind("w-full h-1/5 justify-center items-center")}
>
<View
style={[
tailwind("w-full flex-row p-10 justify-center"),
{ justifyContent: "space-between" },
]}
>
<View style={tailwind("justify-center items-center")}>
<AppText style={{ textDecorationLine: "underline" }}>
Total Likes
</AppText>
<AppText style={{ paddingVertical: 10 }}>{likes}</AppText>
</View>
<View style={tailwind("justify-center items-center")}>
<AppText style={{ textDecorationLine: "underline" }}>
Completed
</AppText>
<AppText style={{ paddingVertical: 10 }}>{completed}</AppText>
</View>
</View>
</View>
<View
style={tailwind("w-full h-4/5 flex-1 items-center")}
>
{responseJson == null ?
<AppText style={tailwind("mt-10")}>
Select events on the "Discover" page!
</AppText>
:
<ListScreen caller={{"sender": "profile", "json": responseJson}}/>
}
</View>
</Screen>
);
};
export default ProfileScreen;
const styles = StyleSheet.create({
logoutButton: {
color: colors.white,
paddingTop: 20,
paddingRight: 10,
},
bg: {
flex: 1,
backgroundColor: colors.black,
},
});
Update: If I set the state twice in a row, it works:
setResponseJson(null);
setResponseJson([...res]);
However, this is somewhat buggy and not optimal. Leads me to think it still is a reference issue, but I am not sure why the spread operator technique does not fix this.

Three things are wrong/weird in your code. Not sure it will fix your problem but here are they :
1- Do not pass an async function to the useEffect. Instead, create an async function inside and call it :
useEffect(() => {
const myFunc = async => {...};
myFunc();
},[]);
2- You are receiving an array of objects from your api call, so why do you want to spread it ? It would lead to multiple objects inside your state, which doesn't seems right. Just pass your res in your state as is or format it as you want.
3- You are mixing async/await pattern with the .then. Either use an async func and await the result :
const myFunc = async () => {
const resPromise = await fetch(...);
const json = await resPromise.json();
...
}
or only use the .then:
const myFunc = () => {
fetch(...)
.then(data => data.json())
.then(json => ...);
...
}

Related

Fetch Inside A Fetch - Objects Are Not Valid As A React Child

I am trying to make two fetch requests. The second one cannot be completed until MemberId is retrieved from the first one. I have tried putting them in separate async functions but keep getting the below error.
Any insights on the best way to do this would be much appreciated.
Error message:
Uncaught Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { StyleSheet, Image, Text, View } from "react-native";
export default async function App() {
const [isLoading, setLoading] = useState(true);
const [isLoadingMp, setLoadingMp] = useState(true);
const [data, setData] = useState([]);
const [mpData, setMpData] = useState([]);
let memberId = 0;
useEffect(() => {
fetch(
`https://members-api.parliament.uk/api/Members/Search?Name=Boris%20Johnson`
)
.then((response) => response.json())
.then((json) => {
setMpData(json);
memberId = json.items[0].value.id;
console.log("memberId:", memberId);
})
.catch((error) => console.error(error))
.then(
fetch(
`https://commonsvotes-api.parliament.uk/data/divisions.json/membervoting?memberId=${memberId}`
)
.then((response) => response.json())
.then((json) => setData(json))
.catch((error) => console.error(error))
.finally(() => setLoading(false))
.finally(() => {
setLoadingMp(false);
})
);
});
return (
<View style={{ flex: 1, padding: 24 }}>
{isLoading || isLoadingMp ? (
<Text>Loading...</Text>
) : (
<View style={styles.container}>
<Text>
<Image
source={{
uri: `${mpData.items[0].value.thumbnailUrl}`,
width: 60,
height: 60,
}}
/>
<Text>{`${mpData.items[0].value.id}\n`}</Text>
{data.map((individualData) => {
return `\nDate: ${
individualData.PublishedDivision.Date
}\nDivision id: ${
individualData.PublishedDivision.Date
}\nDivision title: ${
individualData.PublishedDivision.Title
}\nVoted: ${!!individualData.MemberVotedAye ? "Yes" : "No"}\n`;
})}
</Text>
<StatusBar style="auto" />
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { StyleSheet, Image, Text, View } from "react-native";
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isLoadingMp, setLoadingMp] = useState(true);
const [data, setData] = useState([]);
const [mpData, setMpData] = useState([]);
let memberId = 0;
useEffect(() => {
const callApi = async ()=>{//defining a async function to call the API
setLoading(true);//maybe you just need one loading flag
try{
const result = await (await fetch(
`https://members-api.parliament.uk/api/Members/Search?Name=Boris%20Johnson`
)).json();//first api and json() its result
const memberId = result.items[0].value.id;//get the member id from it
const finalResult = await(await fetch(
`https://commonsvotes-api.parliament.uk/data/divisions.json/membervoting?memberId=${memberId}`
)).json();//second api call and json() its result
//set the state
setData(finalResult);
setMpData(result);
}catch(e){
console.log(e)
}finally{
setLoading(false);//set loading false in both the fail and success case
}
}
callApi();//calling the function
}, []);
//VIEW rendering
return (<View style={{ flex: 1, padding: 24 }}>
{isLoading ? (
<Text>Loading...</Text>
) : (
<View style={styles.container}>
<Text>
<Image
source={{
uri: `${mpData.items[0].value.thumbnailUrl}`,
width: 60,
height: 60,
}}
/>
<Text>{`${mpData.items[0].value.id}\n`}</Text>
{data.map((individualData) => {
return `\nDate: ${
individualData.PublishedDivision.Date
}\nDivision id: ${
individualData.PublishedDivision.Date
}\nDivision title: ${
individualData.PublishedDivision.Title
}\nVoted: ? "Yes" : "No"}\n`;
})}
</Text>
<StatusBar style="auto" />
</View>
)}
</View>);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
This specific problem has nothing to do with the fetches, it is simpler.
You declared your functional component as async:
export default async function App() {
The consequence is that it doesn't return a React element but a Promise that resolves to a React element, which React can't handle.
Remove the async:
export default function App() {

React Native Google / Facebook Login

I'm quit new in React native but I would like to set up Google login but what I did didn't seem to work.
(That will be a long post haha )
For Google Login:
I tried several tutorials on YouTube (this one for example: https://www.youtube.com/watch?v=DN9dQ_6ezvA)
"Request is missing required authentication credential. Expected OAuth
2 access token, login cookie or other valid authentication credential.
See
https://developers.google.com/identity/sign-in/web/devconsole-project."
I tried a lot of thing but it's really hard for me. So if someone could give me some help? Thanks you in advance guys, I continue the research on my side
Here is my code:
WebBrowser.maybeCompleteAuthSession();
export default function GoogleFunc({largeur}) {
const fontRegular = require("../../assets/Manrope-Regular.ttf");
const fontBold = require("../../assets/Manrope-Bold.ttf");
const [fontsLoaded] = useFonts({
ManropeRegular: fontRegular,
ManropeBold: fontBold,
});
const [accessToken, setAccessToken] = React.useState();
const [userInfo, setUserInfo] = React.useState();
const [request, response, prompAsync] = Google.useIdTokenAuthRequest({
androidClientId:
"ANDROID_CLIENT_ID",
iosClientId:
"CLIENT_CLIENT_ID",
clientId:
"CLIENT_ID",
});
React.useEffect(() => {
if (response?.type === "success") {
console.log("response: ", response.params);
setAccessToken(response.params.id_token);
accessToken && fetchUserInfo();
}
}, [response, accessToken]);
async function fetchUserInfo() {
console.log("test222");
let response = await fetch("https://www.googleapis.com/userinfo/v2/me", {
headers: {Authorization: `Bearer ${accessToken}`},
});
const useInfo = await response.json();
console.log("useinfo: ", useInfo);
setUserInfo(useInfo);
}
const ShowUserInfo = () => {
if (userInfo) {
console.log("userinfooofofof: ", userInfo);
return (
<View style={styles.userInfo}>
<Image source={{uri: userInfo.picture}} style={styles.profilePic} />
<Text>Welcome {userInfo.name}</Text>
<Text>{userInfo.email}</Text>
</View>
);
}
};
return (
<View style={styles.container}>
{userInfo && <ShowUserInfo />}
{userInfo == null && (
<>
<TouchableOpacity
style={[styles.googleBtn, {width: largeur}]}
disabled={!request}
onPress={() => {
prompAsync();
}}
>
<Image
style={{
height: height * 0.024,
width: width * 0.057,
left: width * 0.035,
top: height * 0.013,
}}
source={require("../../assets/images/google-logo.png")}
/>
<Text
style={{
textAlign: "center",
fontFamily: "ManropeSemiBold",
fontSize: 17,
color: "#262626",
bottom: height * 0.01,
}}
>
Continuer avec Google
</Text>
</TouchableOpacity>
</>
)}
</View>
);
}
For Facebook:
I tried to set up the login with / without firebase but I already got the same issue : an error ...
I'm trying to solve it since last week but I don't understand how can i fix these problems ...
Here's my code without Firebase:
import React, {useState} from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import * as Facebook from "expo-facebook";
export default function FacebookFunc() {
const [isLoggedin, setLoggedinStatus] = useState(false);
const [userData, setUserData] = useState(null);
const [isImageLoading, setImageLoadStatus] = useState(false);
async function faceookLogIn() {
try {
await Facebook.initializeAsync({
appId: "APP ID",
});
const {type, token, expirationDate, permissions, declinedPermissions} =
await Facebook.logInWithReadPermissionsAsync({
permissions: ["public_profile"],
});
if (type === "success") {
// Get the user's name using Facebook's Graph API
const response = await fetch(
`https://graph.facebook.com/me?access_token=${token}`
);
Alert.alert("Logged in!", `Hi ${(await response.json()).name}!`);
} else {
// type === 'cancel'
}
} catch ({message}) {
alert(`Facebook Login Error: ${message}`);
}
}
logout = () => {
setLoggedinStatus(false);
setUserData(null);
setImageLoadStatus(false);
};
return isLoggedin ? (
userData ? (
<View style={styles.container}>
<Image
style={{width: 200, height: 200, borderRadius: 50}}
source={{uri: userData.picture.data.url}}
onLoadEnd={() => setImageLoadStatus(true)}
// GERER QUAND MARCHERA
/>
<ActivityIndicator
size={"large"}
color={"#0000ff"}
animating={!isImageLoading}
style={{position: "absolute"}}
/>
<Text style={{fontSize: 22, marginVertical: 10}}>
Hi {userData.name}!
</Text>
<TouchableOpacity style={styles.logoutBtn} onPress={logout}>
<Text style={{color: "#fff"}}>Logout</Text>
</TouchableOpacity>
</View>
) : null
) : (
<View style={styles.container}>
<TouchableOpacity style={styles.loginBtn} onPress={faceookLogIn}>
<Text style={{color: "#fff", textAlign: "center"}}>
Login with facebook
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {},
loginBtn: {
backgroundColor: "#4267b2",
paddingVertical: 10,
borderRadius: 10,
width: "95%",
},
logoutBtn: {
backgroundColor: "grey",
includeFontPadding: 10,
paddingHorizontal: 20,
borderRadius: 20,
position: "absolute:",
bottom: 0,
marginBottom: 200,
},
});
And here my code when I trying with Firebase:
The error:
Possible Unhandled Promise Rejection (id: 0):
TypeError: null is not an object (evaluating 'LoginManager.logInWithPermissions')
The code:
import React from "react";
import {LoginManager, AccessToken} from "react-native-fbsdk-next";
import {
StyleSheet,
View,
Text,
Image,
TouchableOpacity,
Button,
} from "react-native";
import {
getAuth,
signInWithCredential,
FacebookAuthProvider,
} from "firebase/auth";
import "expo-dev-client";
import app from "./firebaseSetup";
export default function App() {
const SignInWithFB = async () => {
const result = await LoginManager.logInWithPermissions([
"public_profile",
"email",
]);
if (result.isCancelled) {
throw new Error("User cancelled login");
}
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
throw new Error("Something went wrong obtaining access token");
}
const auth = getAuth(app);
const credential = FacebookAuthProvider.credential(data.accessToken);
const user = await signInWithCredential(auth, credential);
console.log(user);
};
return (
<View>
<Button title="Sign in with facebook" onPress={SignInWithFB}></Button>
</View>
);
}
const styles = StyleSheet.create({});
I initialize my firebaseSetup like this:
import {initializeApp} from "firebase/app";
// Initialize Firebase
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: "",
};
const app = initializeApp(firebaseConfig);
export default app;
I continue my research by my side but if someone could help me with this problem (which is probably stupid), thanks you in advance...
PS: There is the error for Facebook without firebase:
Facebook Login Error: undefined is not an object (evaluating'_ExponentFacebook.default.InitializeAsync')
You have to use expo auth session to implement oAuth in Expo
import React,{useState} from 'react';
import { useNavigation } from "#react-navigation/native";
import Fontisto from '#expo/vector-icons/Fontisto';
import { TouchableOpacity,StyleSheet, Alert } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
import * as Facebook from 'expo-auth-session/providers/facebook';
import {makeRedirectUri, ResponseType } from 'expo-auth-session';
import { useDispatch } from "react-redux";
WebBrowser.maybeCompleteAuthSession();
export default function FacebookAuth(){
const [isLoading, setIsLoading] = useState(false);
const navigation = useNavigation();
const dispatch = useDispatch();
const [request, response, promptAsync] = Facebook.useAuthRequest({
responseType: ResponseType.Token,
clientId: 'YOUR_CLIENT_ID',
scopes: ['public_profile', 'email'],
redirectUri: makeRedirectUri({ useProxy: true })
},
{ useProxy: true });
React.useEffect(() => {
if (response?.type === 'success') {
setIsLoading(true);
const accessToken = response.authentication.accessToken;
console.log(accessToken);
}
}, [response]);
return(
<>
<TouchableOpacity disabled={!request} style={styles.socialLoginButton}
onPress={() => {
promptAsync();
}}
>
<Fontisto name='facebook' color={'white'} size={25}/>
</TouchableOpacity>
</>
)
}
const styles = StyleSheet.create({
socialLoginButton:{
margin:3,
backgroundColor:'blue',
padding:10,
borderRadius:50,
width:50,
height:50,
justifyContent:'center',
alignItems:'center',
margin:10,
}
})
Here's the code for it, you have to configure it according to your needs.
here's the link for expo auth session. - here
For login with Google, I think the problem lies with this code:
React.useEffect(() => {
if (response?.type === "success") {
setAccessToken(response.params.id_token);
accessToken && fetchUserInfo();
}
}, [response, accessToken]);
You use the id_token from the response, and you should be using the access token. Also, I think you could split this into two useEffect methods:
React.useEffect(() => {
if (response?.type === "success") {
setAccessToken(response.params.access_token);
}
}, [response]);
React.useEffect(() => {
fetchUserInfo();
}, [accessToken]);
The other effect will trigger only when the state of accessToken changes. If it's possible that you change the state to an empty value then you should retain the null check.
As for Facebook and Firebase issues — are you sure you followed all the required setup for these libraries? Just skimming through the documentation shows that there are quite a few things that should be configured before you can call the libs. From your error messages, it seems that the libraries are not configured and initialized properly.

Firebase v9 not adding docs

I'm working with Firebase v9. The authentication works fine, but the Firestore does not work me for some reason. I don't even get an error--it just doesn't do anything.
I tried addDocs() but still nothing works.
EDIT: actually , i was using the firebase #9.1.0 i upgraded it to #9.6.7 and it worked perfectly fine ! i had to downgrade from #9.6.8 ( the latest ) to #9.1.0 because of the error ( Can't find variable: IDBIndex ) !
import React, { useLayoutEffect, useState } from "react";
import {
Text,
View,
StyleSheet,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
ScrollView,
Alert,
} from "react-native";
import { AntDesign, Ionicons } from "#expo/vector-icons";
import { doc, setDoc } from "firebase/firestore";
import { db } from "../../firebase/firebaseConfig";
const NewChat = ({ navigation }) => {
const [input, setInput] = useState("");
useLayoutEffect(() => {
navigation.setOptions({
title: "Add a new Chat",
headerBackTitle: "Chats",
});
}, [navigation]);
const AddChat = async () => {
const myDoc = doc(db, "Chats", input);
const docData = {
chatName: input,
};
setDoc(myDoc, docData).then(() => {
navigation.goBack();
});
};
return (
<ScrollView>
<View
style={{
marginTop: 20,
marginHorizontal: 20,
borderColor: "black",
borderWidth: 1,
}}
>
<View style={styles.container}>
<AntDesign
name="wechat"
size={40}
color="black"
style={{ alignSelf: "center" }}
/>
<TextInput
placeholder="Enter a chat name"
value={input}
onChangeText={(text) => {
setInput(text);
}}
style={{ flexGrow: 1, marginLeft: 20 }}
/>
<TouchableOpacity style={{ alignSelf: "center" }} onPress={AddChat}>
<Ionicons name="checkmark-done-circle" size={40} color="black" />
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: "row",
backgroundColor: "white",
justifyContent: "center",
height: 60,
},
});
export default NewChat;
The function setDoc() asynchronously returns a promise which means all you're missing is the await keyword before you call the function.
const AddChat = async () => {
const myDoc = doc(db, "Chats", input);
const docData = {
chatName: input,
};
await setDoc(myDoc, docData).then(() => {
navigation.goBack();
});
};
Edit: I think I see the real problem, It has to do with the v9 document reference. Try using collection() within the document reference.
const AddChat = async () => {
const myDoc = doc(collection(db, "Chats"), input);
const docData = {
chatName: input,
};
await setDoc(myDoc, docData).then(() => {
navigation.goBack();
});
};

useState is not working properly in React Native

I'm working on an application and using WordPress API for showing posts. I've created 2 buttons to navigate the list of posts. As you know there is an argument "page=" to get posts on a specific page, I've initialized a state to maintain page number. The main problem is that it's not incrementing correctly.
Post Screen Code -
import React, { useState, useEffect } from "react";
import { View, FlatList, TouchableOpacity } from "react-native";
import { Colors } from "../constant/colors";
import globalStyles from "../constant/globalStyle";
import axios from "axios";
import PostCard from "../components/PostCard";
import CustomButton from "../components/Button";
const Updates = () => {
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await axios.get(
`https://bachrasouthpanchayat.in/wp-json/wp/v2/posts?embed=true&page=${page}`
);
setData(response.data);
setLoaded(true);
};
const previousHandler = () => {
setLoaded(false);
let newPage = page - 1;
setPage(newPage);
fetchData();
};
const nextHandler = () => {
setLoaded(false);
let newPage = page + 1;
setPage(newPage);
fetchData();
};
return (
<View
style={{
...globalStyles.container,
backgroundColor: Colors.BACKGROUND_SCREEN,
}}
>
{loaded ? (
<>
<FlatList
style={{ flex: 1, margin: 10 }}
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity activeOpacity={0.7}>
<PostCard
title={item.title.rendered}
imageUrl={item.jetpack_featured_media_url}
/>
</TouchableOpacity>
);
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "center",
alignContent: "stretch",
justifyContent: "center",
}}
>
{page == 1 ? (
<TouchableOpacity
activeOpacity={0.7}
style={{ width: "100%" }}
onPress={nextHandler}
>
<CustomButton>Next</CustomButton>
</TouchableOpacity>
) : (
<>
<TouchableOpacity
activeOpacity={0.7}
style={{ marginRight: 2, width: "50%" }}
onPress={previousHandler}
>
<CustomButton>Previous</CustomButton>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
style={{ width: "50%" }}
onPress={nextHandler}
>
<CustomButton>Next</CustomButton>
</TouchableOpacity>
</>
)}
</View>
</>
) : null}
</View>
);
};
export default Updates;
I had logged state in every state and found that was not incrementing from 1 to 2 on pressing the button the first time. I think state updated after API call because both buttons had started showing even I've used condition to show both buttons only if the state is not 1
Please let me know if i've made any silly mistake 😂😂
useEffect(() => {
fetchData();
}, []);
The last argument to useEffect is an array of dependencies so that React will only re-run the effect when the dependencies have changed. You are passing an empty array, which tells React that there are no dependencies and the effect should only be run once, when the component is first mounted.
Now you actually want the effect to re-run when the page changes, so you should put page in the depenency array:
useEffect(() => {
fetchData();
}, [page]);
And (credit: #Keith) you should remove the extra fetchData() calls in the nextHandler and previousHandler
You have to implement it like this:
useEffect(() => {
const fetchData = async () => {
setLoaded(true);
const response = await axios.get(
`https://bachrasouthpanchayat.in/wp-json/wp/v2/posts?embed=true&page=${page}`
);
setData(response.data);
setLoaded(false);
};
fetchData();
}, [page]);
const previousHandler = () => {
setPage(prevPage => prevPage - 1);
};
This way whenever the user changes the page, it will automatically call the function in useEffect, since it is in the dependency array.

react native and useFocusEffect duplicate data when navigating back

I am struggling with useFocusEffect or useEffect. I am trying to fetch data when user navigate to the user profile this is working but when user navigate back to the profile i have a duplicate posts! I've been working on this problem for 3 days. Please any ideas.
This is the code:
import React, { useState, useEffect, useRef } from "react";
import {
View,
Text,
FlatList,
Image,
TouchableOpacity,
Animated,
ActivityIndicator,
} from "react-native";
import APIKIT from "../../../services/APIKit";
import styles from "./UserPostStyle";
import moment from "moment";
import { MaterialCommunityIcons, FontAwesome } from "#expo/vector-icons";
import { useNavigation } from "#react-navigation/native";
import { useFocusEffect } from "#react-navigation/native";
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
moment.updateLocale("en", {
relativeTime: {
future: "in %s",
past: "%s ago",
s: "Just now",
ss: "%ss",
m: "1m",
mm: "%dm",
h: "1h",
hh: "%dh",
d: "1d",
dd: "%dd",
M: "1mo",
MM: "%dmo",
y: "1y",
yy: "%dy",
},
});
export function UserPosts({ ...props }) {
const [userPosts, setUserPosts] = useState([]);
const [posts, setPosts] = useState([]); // Array with the posts objects
let postsArray = useRef(posts).current; // Auxiliar array for storing posts before updating the state. Pd: this will not re-render the component as it is a reference
const [page, setPage] = useState(1);
const [lastPage, setLastPage] = useState(0);
const [loadingMore, setLoadingMore] = useState(false);
const navigation = useNavigation();
useFocusEffect(
React.useCallback(() => {
if (props.userId) {
fetchPosts();
}
return () => {};
}, [props.userToken, props.userId, page])
);
// useEffect(() => {
// console.log(" -----------useEffect page", page);
// console.log(" -----------useEffect lastPage", lastPage);
// let mounted = true;
// if (mounted && props.userId) {
// fetchPosts();
// }
// return () => {
// console.log("return useEffect page", page);
// console.log("return useEffect lastPage", lastPage);
// mounted = false;
// };
// }, [props.userToken, props.userId, page]);
const fetchPosts = async () => {
if (props.userToken !== null) {
const config = {
headers: { Authorization: `Bearer ${props.userToken}` },
};
APIKIT.get(`/user/posts/${props.userId}?page=${page}`, config)
.then((response) => {
let posts = [];
const { data } = response;
setLastPage(data.userPosts.last_page);
for (let userPost of data.userPosts.data) {
let post = {};
post["id"] = userPost.id;
post["location_coordinate"] = userPost.location_coordinate;
post["location_icon"] = userPost.location_icon;
post["location_id"] = userPost.location_id;
post["location_title"] = userPost.location_title;
post["location_vicinity"] = userPost.location_vicinity;
post["post_body"] = userPost.post_body;
post["created_at"] = moment(userPost.created_at).fromNow(true);
post["user_id"] = userPost.user_id;
posts.push(post);
}
posts.forEach((newPost) => {
// Add the new fetched post to the head of the posts list
postsArray.unshift(newPost);
});
setPosts([...postsArray]); // Shallow copy of the posts array to force a FlatList re-render
setUserPosts((prevPosts) => [...prevPosts, ...posts]);
setLoadingMore(false);
if (page === lastPage) {
setLoadingMore(false);
return;
}
})
.catch((e) => {
console.log("There is an error eccured while getting the posts ", e);
setLoadingMore(false);
});
}
};
const Item = ({
post_body,
id,
location_coordinate,
location_icon,
location_title,
location_vicinity,
location_id,
created_at,
}) => (
<View style={styles.postContainer}>
<TouchableOpacity
onPress={() => {
navigation.navigate("PostDetailsScreen", {
location_coordinate: JSON.parse(location_coordinate),
userAvatar: props.userAvatar,
username: props.username,
name: props.name,
created_at: created_at,
post_body: post_body,
location_title: location_title,
location_vicinity: location_vicinity,
location_icon: location_icon,
location_id: location_id,
});
}}
>
<View style={styles.postHeader}>
<Image
style={styles.userAvatar}
source={
props.userAvatar
? { uri: props.userAvatar }
: require("../../../../assets/images/default.jpg")
}
/>
<View style={styles.postUserMeta}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text style={styles.name}>{props.name}</Text>
<Text style={styles.createdAt}>{created_at}</Text>
</View>
<Text style={styles.username}>#{props.username}</Text>
</View>
</View>
<View style={styles.postContent}>
<View>
<Text style={styles.postBody}>{post_body}</Text>
</View>
</View>
</TouchableOpacity>
<TouchableOpacity
style={styles.locationInfoContainer}
onPress={() => {
navigation.navigate("PostPlaceDetailsScreen", {
location_coordinate: JSON.parse(location_coordinate),
userAvatar: props.userAvatar,
username: props.username,
name: props.name,
created_at: created_at,
post_body: post_body,
location_title: location_title,
location_vicinity: location_vicinity,
location_icon: location_icon,
location_id: location_id,
});
}}
>
<View style={styles.locationInfo}>
<Image style={styles.locationIcon} source={{ uri: location_icon }} />
<View style={styles.locationMeta}>
<Text style={styles.locationTitle}>{location_title}</Text>
</View>
</View>
</TouchableOpacity>
<View
style={{
borderWidth: 1,
borderColor: "#F2F2F2",
marginTop: 10,
marginBottom: 10,
}}
/>
<View style={styles.postFooter}>
<TouchableOpacity>
<MaterialCommunityIcons name="comment" size={24} color="#999999" />
</TouchableOpacity>
<TouchableOpacity>
<FontAwesome name="star" size={24} color="#999999" />
</TouchableOpacity>
{/* After fav color #FFBE5B */}
<TouchableOpacity>
<MaterialCommunityIcons
name="dots-horizontal"
size={24}
color="#999999"
/>
</TouchableOpacity>
</View>
</View>
);
const renderItem = ({ item }) => (
<Item
location_coordinate={item.location_coordinate}
post_body={item.post_body}
id={item.id}
location_id={item.location_id}
location_icon={item.location_icon}
location_title={item.location_title}
location_vicinity={item.location_vicinity}
created_at={item.created_at}
/>
);
const emptyPosts = () => {
return (
<View style={styles.noPostsMessageContainer}>
<View style={styles.messageContainer}>
<Image
style={styles.noPostMessageImage}
source={require("../../../../assets/images/Logo.png")}
/>
<Text style={styles.noPostMessageText}>No posts yet!</Text>
</View>
</View>
);
};
const handleLoadingMore = () => {
if (page === lastPage) {
return;
}
setPage(page + 1);
setLoadingMore(true);
};
return (
<View style={styles.userPostContainer}>
<AnimatedFlatList
showsVerticalScrollIndicator={false}
data={userPosts}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{
paddingTop: 250,
paddingBottom: 100,
}}
scrollEventThrottle={16}
onScroll={props.scrolling}
ListEmptyComponent={emptyPosts}
onEndReachedThreshold={0.5}
onMomentumScrollEnd={() => handleLoadingMore()}
ListFooterComponent={
loadingMore && <ActivityIndicator size="large" animating />
}
/>
</View>
);
}
FlatList
return (
<View style={styles.userPostContainer}>
<AnimatedFlatList
showsVerticalScrollIndicator={false}
data={userPosts}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{
paddingTop: 250,
paddingBottom: 100,
}}
scrollEventThrottle={16}
onScroll={props.scrolling}
ListEmptyComponent={emptyPosts}
onEndReachedThreshold={0.5}
onMomentumScrollEnd={() => handleLoadingMore()}
ListFooterComponent={
loadingMore && <ActivityIndicator size="large" animating />
}
/>
</View>
);
handleLoadingMore function
const handleLoadingMore = () => {
if (page === lastPage) {
return;
}
setPage(page + 1);
setLoadingMore(true);
};
I think the problom is when the user navigate back to the profile the state still the same and useFocusEffect fetching the same data again.
setUserPosts((prevPosts) => [...prevPosts, ...posts]);
Thanks for you help.
I have never used this hook from "react-navigation" but in the doc it says:
Sometimes we want to run side-effects when a screen is focused. A side
effect may involve things like adding an event listener, fetching
data, updating document title, etc. While this can be achieved using
focus and blur events, it's not very ergonomic.
So, every time you focus an specific route the code will run.
I am trying to fetch data when user navigate to the user profile this
is working but when user navigate back to the profile i have a
duplicate posts!
What you are doing is wrong because you are not paginating your fetches, I mean, you will need a "pointer" to the last post you fetched... with this you will avoid to fetch the data you already have. Also, the user experience will be pretty faster, as you will have lighter responses from your DB.
Anyways, I suggest you to run this code in the "useEffect" hook from React Native and try to implement a db listener or an inifinite scroll with pagination. This will not work when you focus the screen, but you will be able to fetch data every time the user refresh the FlatList, just like Instagram, Twitter, Netflix...
Take a look on this: https://aboutreact.com/react-native-flatlist-pagination-to-load-more-data-dynamically-infinite-list/
If you really need to fetch data when you focus the specific route, just implement a pagination (save in your component state an index to the last post you fetched).
UPDATE
Sorry, I didn't see that you was doing the pagination in your code. It might be a problem when you update the state of your component...
Try something like that:
const [posts, setPosts] = useState([]); // Array with the posts objects
let postsArray = useRef(posts).current; // Auxiliar array for storing posts before updating the state. Pd: this will not re-render the component as it is a reference
const loadMorePosts = () => {
// ... stuff
// const newPosts = Fetch posts
newPosts.forEach((newPost) => {
// Add the new fetched post to the head of the posts list
postsArray.unshift(newPost);
})
// The last post will be at the top of the list
setPosts([...postsArray]); // Shallow copy of the posts array to force a FlatList re-render
}
// setUserPosts((prevPosts) => [...prevPosts, ...posts]);

Categories

Resources