React Native component not re-rendering when state is updated - react-native

I have a component in my React Native app that displays a list of pending friends. This component makes a GET request to an API to retrieve the list of pending friends and then uses a useEffect hook to map over the list and render each friend as a Pressable component. I'm also using the useFocusEffect hook to make the get request when the screen renders.
Here is the relevant code for the component:
const Pending = () => {
const [pendingFriends, setPendingFriends] = useState(null)
let pendingFriendsRender = []
useEffect(() => {
if (pendingFriends !== null) {
for(let i = 0; i < pendingFriends.length; i++) {
pendingFriendsRender.push(
<Pressable key={i} style={styles.friend}>
<Text style={styles.friendText}>{pendingFriends[i].username}</Text>
</Pressable>
)
}
}
}, [pendingFriends])
useFocusEffect(
useCallback(() => {
async function fetchData() {
const accessToken = await AsyncStorage.getItem('accessToken')
try {
const res = await instance.get('/pending_friends', {
headers: { authorization: `Bearer ${accessToken}`},
})
setPendingFriends(res.data)
} catch (error) {
console.log(error.response.status)
}
}
fetchData()
}, [])
)
return(
<View style={styles.friendsContainer}>
{pendingFriendsRender}
</View>
)
}
I have tried using an empty array as the second argument in the useEffect hook but that approach has not worked. I also tried removing the useEffect hook so the if statement with the for loop stands at the top of the component without the hook, that worked but I can't update it in this way after the component rendered. I checked the API and it is returning the correct data.

The first useEffect you have really isn't needed. You can map through your state inside of your JSX. Anytime the state changes, the component will be re-rendered:
// Need a default here, could also set some loading state when fetching your data
if(pendingFriends === null) {
return <>Loading...</>
}
return(
<View style={styles.friendsContainer}>
{pendingFriends.map((friend, i) => {
return (
<Pressable key={friend.id} style={styles.friend}>
<Text style={styles.friendText}>{friend.username}</Text>
</Pressable>
)
})}
</View>
)
Also keep in mind, it's not recommended to use the index as the key, it can lead to unexpected bugs and issues. Instead use a unique string key (as shown above).
React: using index as key for items in the list

pendingFriendsRender should be the state:
const [pendingFriendsRender, setPendingFriendsRender] = useState([])
Instead of
let pendingFriendsRender = []
Then just clone the array so you lose reference to the object and add the new element
const newPendingFriendsRender = [...pendingFriendsRender, newElement]
or you can use FlatList to make it easier.

Related

Cannot update a component while rendering a different component react-navigation

I'm building an app using expo and react native. In my project, I have a stackNavigator containing three stackScreen and I also have a DatabaseManager class with static async function. In my components, I use useState and useEffect to load my data and display them.
When I move from the first Screen to the second one, I got an error :
Warning: Cannot update a component (%s) while rendering a different component (%s). To locate the bad setState() call inside %s, follow the stack trace as described in https://reactjs.org/link/setstate-in-render%s, StackNavigator, Screen, Screen.
This warning is, I suppose the reason why I need to tap 2 times on the screen to navigate between screens sometimes.
Here's a part of my code:
export default class DatabaseManager {
...
static async getSplits(){
if (!this.db)
await this.openDatabase();
const query = "select distinct day from exercises";
const resultQuery = await this.executeSql(query, []);
const result = [];
var rows = resultQuery.rows;
for (let i = 0; i < rows.length; i++) {
var item = rows.item(i);
result.push({split: item.DAY});
}
return result;
}
static async getExercises( split){
if (!this.db)
await this.openDatabase();
const query = "select distinct name from exercises where day = ? order by number";
const resultQuery = await this.executeSql(query, [split]);
const result = [];
var rows = resultQuery.rows;
for (let i = 0; i < rows.length; i++) {
var item = rows.item(i);
result.push({name: item.NAME});
}
return result
}
}
App.js
//...
function HomeStackScreen() {
return (
<HomeStack.Navigator initialRouteName='Home' >
<HomeStack.Screen name="Home" component={Home} options={{title:'Splits'} }/>
<HomeStack.Screen name="Screen" component={Screen} />
</HomeStack.Navigator>
);
}
...
export default function Home ({navigation}){
const [splits, setSplits] = useState([{ split:''}]);
const handlePress = async (split) => {
navigation.navigate('Screen', {split: split});
};
useEffect(() => {
const dataFetch = async () => {
const data = await DatabaseManager.getSplits();
setSplits(data);
}
dataFetch();
}, [])
return (
<View style={styleHome.container}>
<ScrollView style={styleHome.scrollViewContainer}>
{splits.map((split, index) => (
<View style={styleHome.touchableContainer} key={index}>
<Pressable
key={split.split}
style={styleHome.touchable}
activeOpacity={1}
onPress={() => handlePress(split.split)}
>
<Text style={styleHome.textTouchable}>{split.split}</Text>
</Pressable>
</View>
))}
</ScrollView>
</View>
);
The second screen is similar to this.
I tried to use useFocusEffect, enableFreeze and useFocus. I also tried to put a Loading Screen before navigating to a new screen, nothing solve the issue. By using some console.log, I found out that my code seems to be correct, the Home component doesn't re render after navigating to the other component.
I found out the solution: I just had a bad navigation.setOptions() in the second screen ...
I wrapped it in the useEffect() and it works fine.
I found the solution there: https://github.com/react-navigation/react-navigation/issues/9478#issuecomment-813031134

AsyncStorage issue in react native expo

I can't even tell you how many variations I have tried, tutorials and documentations that I have watched and read, I cannot transfer data from one page to another. Am using react native expo. I have this included in both pages: import AsyncStorage from '#react-native-async-storage/async-storage';.
This is the page I'm trying to set the data:
const ToyDetails = () => {
const [savedName, setSavedName] = useState('')
const addCart = async() => {
setButtonText('ADDED TO CART!')
try {
await AsyncStorage.setItem('saved_name', savedName)
}catch(error){
console.log(error)
}
}
return(
<View>
<Text value={savedName}>{name}</Text>
#{name} is because I am importing the name from a FlatList item
</View>
)
}
And getting that data from another page:
const Cart = () => {
const [savedName, setSavedName] = useState('')
useEffect(()=>{
getData()
}, [])
const getData = () => {
try {
AsyncStorage.getItem('saved_name')
.then((value)=>{
if(value!=null){
setSavedName(value)
}
})
}catch(error){
console.log(error)
}
}
return (
<View>
<Text value={savedName} onChangeText={(value)=>setSavedName(value)}>{savedName}</Text>
</View>
)
}
I can post other variations I have tried if asked, I've tried adding it into a list and importing the list in the second page, I've tried to JSON.stringify the value savedName first (and JSON.parse it), I even tried doing it in the same way I did for FlatList. I'm not even getting any error messages.
in your ToyDetails.js while saving savedName is empty. i changed to name and able to get it on CartScreen
https://snack.expo.dev/7ozHrsOBT check ToyDetails.j file
const addCart = async() => {
setButtonText('ADDED TO CART!')
try {
console.log("savedName",savedName) //saved name is empty here
await AsyncStorage.setItem('saved_name', name)
}catch(error){
console.log('setitem didnt work')
}
}

Re render flat list when data change cause infinite loop React Native

I have two screens. Approve List and Approve Detail. When data approved in Approve Detail, page navigate to Approve List. Then approved data should disapear from FLatList. How to remove FlatList item when data approved? or how to re render FlatList when data change? Here is my code:
Approve List:
const Approve = ({ navigation }) => {
const [rekomendasi, setRekomendasi] = useState({})
// other code
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getToken();
getUserData()
getRekomendasi(token, userData.bagian);
}, [setToken, setUserData, rekomendasi]); // if I pass rekomendasi here, make infinite loop on api request
return (
<FlatList
onRefresh={() => onRefresh()}
refreshing={isFetching}
removeClippedSubviews
style={{ marginTop: 2 }}
data={rekomendasi}
keyExtractor={rekom => rekom.penjaminan.nomor_rekomendasi}
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={() => navigation.navigate("ApproveDetail", { id: item.penjaminan.nomor_rekomendasi, bagian: userData.bagian })}>
<ApproveList
plafond={item.value}
kantor={item.nama_kantor}
nomor_rekomendasi={item.nomor_rekomendasi}
produk={item.skim}
/>
</TouchableOpacity>
)
}}
showsHorizontalScrollIndicator={false}
/>
)
}
If I pass value on second argument on UseEffect, it cause infinite loop on API request. If not, my FlatList cant re render when data change. What should I do?
Thanks for help
You have to remove the rekomendasi dependency in the useEffect to avoid infinite loop, it's only for init data :)
What is the purpose of onRefresh function in the FlatList ? Instead you could put the getRekomendasi function to trigger a new call and your data will be updated
try to separate the functions to two useEffects
useEffect(() => {
//<-- write your getToken() and getUserDate() here
getToken();
getUserData()
}, []);
useEffect(() => {
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
getRekomendasi(token, userData.bagian);
},[token,userData.bagian]);
Problem solved by using useFocusEffect
useFocusEffect(
React.useCallback(() => {
getRekomendasi(token, userData.bagian)
}, [token, userData.bagian])
);

How to update state on App start in react native

I am trying to read from the AsyncStorage and update my store ( using easy-peasy ) on app start.
My thought process was to call fetch the data from AsyncStorage using useEffect with the second argument of an empty array to only fetch once on app start and update the store with that value using an action.
But that doesn't work i get the error invalid hook call. Any insights on how to solve this or what the correct approach would be are appreciated !
App.ts
export default function App() {
useEffect(() => {
readData();
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};
STORE
initializeState: action((state, payload) => {
state.secret = payload.secret
}),
ERROR
Failed to fetch the input from storage [Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.]
export default function App() {
useEffect(() => {
setTimeout(()=>{ // add setTimeout() may be this work for you
readData();
},500)
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};
You need to move you readData function into the App component since you're using a hook (useStorageActions) inside that function and you can only call hooks at the top level. You should take a look at the rules of react hooks.

React Native: Cant rerender Image after state change

I am starting with react-native.
I am requesting a gif from the giphy API and then updating my giphyUrl in state (the state is changed) but the gif doesnt change ( the component is not rerendered).
class QuoteList extends Component {
state = { quotes: [],
giphyUrl: 'https://media.giphy.com/media/nZQIwSpCXFweQ/giphy.gif'
};
componentWillMount() {
console.log('Again?')
axios.get('https://api.tronalddump.io/search/quote?query='+this.props.characterName)
.then(response => this.setState({ quotes: response.data._embedded.quotes }))
this.getGiphy()
}
getGiphy() {
console.log('getgif')
const GiphyUrl = "https://api.giphy.com/v1/gifs/search?api_key=tutu&limit=1&q=" + this.props.characterName.replace(" ", "+");
console.log(GiphyUrl)
axios.get(GiphyUrl)
.then(response => {
console.log(response)
console.log(response.data.data[0].url)
this.setState({ giphyUrl: response.data.data[0].url })
console.log(this.state)
})
}
renderQuotes() {
return this.state.quotes.map(
quote => <QuoteDetail key={quote.quote_id} quote={quote}/>
);
}
render() {
return (
<ScrollView>
<Image
source={{uri: this.state.giphyUrl}}
style={styles.gifStyle}
/>
{this.renderQuotes()}
</ScrollView>
);
}
}
Why is the component not rerendering ? when I console.log the state in the callback of the axios request, I can see that the state is changed. Even when I try to "force" rerender (forceUpdate), it wouldnt rerender.
Try updating the key property of the image:
<Image
source={{uri: this.state.giphyUrl}}
key={this.state.giphyUrl}
style={styles.gifStyle}
/>
Adding the key prop to any view will cause a rerender of the view as long as the key is changing.