Component not re-rendering on useState array change - react-native

I have a favorite button on the 'tweet' card that I show on the FeedScreen.js.
~~~~~~~~~ IMPORTS SNIP ~~~~~~~~~
function FeedScreen(props) {
const [feed, setFeed] = useState([]);
const [favorites, setFavorite] = useState([]);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadFeed(0, 4);
}, []);
const loadFeed = async (last_id = 0, limit = 1) => {
setRefreshing(true);
const response = await tweetsApi.getTweets(last_id, limit);
if (response.ok) {
setFeed(response.data["data"].concat(feed));
} else {
console.log(response.problem);
}
setRefreshing(false);
};
const handleBookmark = async (item_id) => {
const response = await tweetsApi.toggleBookmark(item_id);
if (response.ok) {
console.log("ok response");
setFavorite(favorites.concat(item_id));
// I've tried this as well
// setFavorite([...favorites].concat(item_id));
// but in vain
console.log(favorites);
}
};
return (
<Screen style={styles.screen}>
<FlatList
data={feed}
keyExtractor={(tweet) => {
return tweet.id.toString();
}}
renderItem={({ item }) => (
~~~~~~~~~ SNIP ~~~~~~~~~
<CardFooter
style={{ marginLeft: 20 }}
item={item}
onPress={handleBookmark}
/>
</View>
)}
ItemSeparatorComponent={ListItemSeparator}
refreshing={refreshing}
onRefresh={() => {
loadFeed(feed[0]["id"], 2);
}}
/>
</Screen>
);
}
~~~~~~~~~ SNIP ~~~~~~~~~
And here's the CardFooter.js :
~~~~~~~~~ SNIP ~~~~~~~~~
function CardFooter({ item, onPress }) {
return (
<View style={styles.bookmark}>
<TouchableOpacity
onPress={() => {
return onPress(item.id);
}}
>
{item.bookmarked && (
<FontAwesome name="bookmark" size={24} color="red" />
)}
{!item.bookmarked && (
<FontAwesome5 name="bookmark" size={24} color="black" />
)}
</TouchableOpacity>
</View>
</View>
);
}
export default CardFooter;
~~~~~~~~~ SNIP ~~~~~~~~~
However the component doesn't seem to re render.
I've looked at these :
react-component-not-re-rendering-after-using-usestate-hook
Similar here
Another one 17 days back - why-usestate-is-not-re-rendering
usestate-not-re-rendering-when-updating-nested-object
All of these and similar other ones, each one of them point to the fact that the a new array should be created so that react re-renders it.
Update
console.log output
yes the console.log is printing the array, although one value previous. That's because useState is async so it isn't printing the realtime array. So, when the second time this is called, it would show one item_id ( the previous one ) added to favorites

I finally solved this by managing the state in the component itself.
Not sure if this is 'the proper way' to do this, but read here (how-to-add-a-simple-toggle-function-in-react-native) that this is how you can do this.
So, now the bookmark component gets its response from the top level component ( FeedScreen.js ) :
const handleBookmark = async (item_id) => {
const response = await tweetsApi.toggleBookmark(item_id);
if (response.ok) {
return true;
} else {
return false;
}
};
And changing the CardFooter.js i.e. where the bookmark component resides.
function CardFooter({ item, onPress }) {
const [favorite, setFavorite] = useState(item.bookmarked);
return (
<View style={styles.bookmark}>
<TouchableOpacity
onPress={async () => {
let response = await onPress(item.id);
if (response) {
setFavorite(!favorite);
} else {
alert("Some error occurred");
}
}}
>
{favorite && <FontAwesome name="bookmark" size={24} color="red" />}
{!favorite && (
<FontAwesome5 name="bookmark" size={24} color="black" />
)}
</TouchableOpacity>
</View>
</View>
);
}
Concerns
I am a bit concerned about handling the response in this component.
Should I handle the async operation in the bottom component ?

Related

Is it possible to render/return React Native elements from functions?

so i want to load some data from my server using axios in React native. The data was retrieved successfully, but i don't know how to display it on the page. When i click button 'Load students' it does axios get method and after that calls method 'showStudents' but that method doesn't return anything. I really don't understand how rendering works in react native so i would appreciate any help and guidance. Also if there is easier way to do all of this, i'm open for suggestions.
export default function Students() {
const [s, setStudents] = useState('')
const getStudents = async () => {
try{
const {data: {students}} = await axios.get('http://192.168.1.2:3000/api/v1/students')
setStudents(students)
//console.log(students)
showStudents()
}
catch(error){
console.log(error)
}
}
const showStudents = () => {
return( <ScrollView>
{
s.map((student) => (
<ListItem key={student._id} bottomDivider>
<ListItem.Content>
<ListItem.Title>{student.firstName}</ListItem.Title>
<ListItem.Subtitle>{student.index}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
))
}
</ScrollView>)
}
return (
<View style={styles.container}>
<Button title='Load students' color='green' onPress={getStudents}/>
</View>
);
}
The function showStudents returns a JSX component, but not inside of the render function of the component Students.
You can just create a new JSX component and use conditional rendering in order to render it whenever the state s (I would call it students) is not undefined and has a length strictly greater than zero.
const [students, setStudents] = useState()
const getStudents = async () => {
try{
const {data: {students}} = await axios.get('http://192.168.1.2:3000/api/v1/students')
setStudents(students)
}
catch(error){
console.log(error)
}
}
return (
<View style={styles.container}>
<Button title='Load students' color='green' onPress={getStudents}/>
{
students && students.length > 0 ? <ScrollView>
{
students.map((student) => (
<ListItem key={student._id} bottomDivider>
<ListItem.Content>
<ListItem.Title>{student.firstName}</ListItem.Title>
<ListItem.Subtitle>{student.index}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
))
}
</ScrollView> : null
}
</View>
);
We could create a new component to make things more structured. Let us introduce StudentList.
export function StudentList({students}) {
return <ScrollView>
{
students.map((student) => (
<ListItem key={student._id} bottomDivider>
<ListItem.Content>
<ListItem.Title>{student.firstName}</ListItem.Title>
<ListItem.Subtitle>{student.index}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
))
}
</ScrollView>
}
Then, reuse this new component.
const [students, setStudents] = useState()
const getStudents = async () => {
try{
const {data: {students}} = await axios.get('http://192.168.1.2:3000/api/v1/students')
setStudents(students)
}
catch(error){
console.log(error)
}
}
return (
<View style={styles.container}>
<Button title='Load students' color='green' onPress={getStudents}/>
{
students && students.length > 0 ? <StudentList students={students} /> : null
}
</View>
);

how do I update useState immediately?

I'm trying to add and remove items from my movies favlist but I am unable to render things immediately with useState. I also trying to update favoritesFilm in UseEffect but my page crashed for continuing re-render.
This is my fav component:
export default function FavouriteBox() {
const navigation = useNavigation<NavigationProps>()
const [favoritesFilm, setFavorite] = useState<Movie[]>([])
const [isLoadingFav, setIsLoadingFav] = useState(true)
useEffect(() => {
getFav()
}, [])
useEffect(() => {
console.log(favoritesFilm)
}, [favoritesFilm])
async function removeMovie() {
const removed = StorageResources.storageRemove('favmovies')
setFavorite(favoritesFilm)
}
async function getFav() {
const favoriteMovies = await StorageResources.storageGet('favmovies')
setFavorite(favoriteMovies)
setIsLoadingFav(false)
}
const renderItemFav = ({ item }: any) => (
<FavMovie name={item.name} title={item.title} poster_path={item.poster_path} id={item.id} />
)
const FavMovie = ({ title, poster_path, name, id }: any) => (
<View style={styles.wrap}>
<Image
style={styles.image}
source={{
uri: `https://image.tmdb.org/t/p/w500/${poster_path}`,
}}
/>
{title && <Text style={styles.fav}>{title}</Text>}
{!title && <Text style={styles.fav}>{name}</Text>}
<MaterialCommunityIcons onPress={() => removeMovie()} name="bookmark-minus-outline" style={styles.book} />
</View>
)
return (
<View style={styles.container}>
<Text style={styles.title}>Preferiti</Text>
{isLoadingFav && <LoaderBox />}
{!isLoadingFav && (
<FlatList
data={favoritesFilm}
keyExtractor={(item) => item.id}
renderItem={renderItemFav}
horizontal
></FlatList>
)}
</View>
)
}
In my home component I use this function to add to fav:
const addToFavorites = async (item: Movie) => {
if (favorites.includes(item)) return null
StorageResources.storageSave('favmovies', [...favorites, item])
setFavorites([...favorites, item])
}
I would like to understand why it doesn't work and why every time I want to show movies in the favmovies component I have to refresh. (I used AsyncStorage for getItem and removeItem)

Adding Search Bar into Flatlist using Hook, Undefined

i try to create flatlist inside modal with search bar functionality that can filter the result based on user input in the search bar.
For the flatlist everything show up accordingly,
problem i can't filter the data,
i try using .filter from the original full data (list) but result is always undefined is not a data.
Btw data is populate from local .json file to state using useEffect.
Here is the local country.json data :
[
"Japan",
"Korea",
"Thailand",
"Indonesia" ]
Here is part of the source code :
import dataCountry from '../../assets/json/country.json';
const NameScreen = ({ navigation }) => {
// hook
const [list, setList] = useState([]);
const [modalBirthplace, setModalBirthplace] = useState(false);
const [searchText, setSearchText] = useState('');
useEffect(() => {
setList({ dataCountry });
console.log('check List : ', list);
}, [])
const renderItem = ({ item }) => (
<Item title={item.title} />
);
const ListItem = ({ title }) => (
<View>
<TouchableOpacity onPress={() => console.log("ok")}>
<Text style={styles.cityList}>{title}</Text>
</TouchableOpacity>
</View>
);
const searchFilterFunction = searchText => {
setSearchText(searchText);
console.log(searchText)
const newData = list.filter((item) => { // error trigger from this list.filter undefined is not a function
const itemData = item.toUpperCase();
const textData = searchText.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setList(newData);
};
return (
<View style={styles.container}>
<Modal
animationType="slide"
transparent={true}
visible={modalBirthplace}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>Choose your country location :</Text>
<TextInput
placeholder="Try japan maybe?"
onChangeText={searchText => searchFilterFunction(searchText)}
value={searchText}
/>
<FlatList
data={list.dataCountry}
renderItem={({ item }) => (
<ListItem
title={item}
/>
)}
keyExtractor={item => item}
/>
<TouchableHighlight
style={{ ...styles.openButton, backgroundColor: '#E15C72' }}
onPress={() => {
setModalBirthplace(!modalBirthplace);
}}>
<Text style={styles.textStyle}>Close Selection</Text>
</TouchableHighlight>
</View>
</View>
</Modal>
</View>
)
}
Anybody know why i can't filter the state?
Thanks a lot before
the problem is your state is an JSON object, not an array:
setList({ dataCountry });
// so list is:
{
dataCountry: [
...
]
}
so, you need to change here
const newData = list.dataCountry.filter((item) => { // here
const itemData = item.toUpperCase();
const textData = searchText.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setList({dataCountry: newData}); // and here
maybe your json like this,
const list = {
dataCountry : [
'UK',
'US'
]
}
List is an object you can't use the filter with an object.
Instead of using array placeholder you can use spread operator like this,
const newData = [...list.dataCountry].filter((item) => {
const itemData = item.toUpperCase();
const textData = searchText.toUpperCase();
return itemData.indexOf(textData) > -1;
});

fetch API call in react native is not working when I load the screeen

I have made an App in react native. My app makes API calls to my webserver and then Displays information based on that. The problem Is when I first load this screen... I get the loading screen and the information is display in the way it is supposed to but when I leave the screen and then comeback to the screen, it shows nothing and my array containing the items is empty, hence I think I am having problems with the API call when I leave the screen and then come back.
I am using React navigation 5 in My App.
export default function ({ navigation }) {
const [openQueries, setOpenQueries] = useState([]);
const [isLoading, seIsLoading] = useState(true);
const open_queries = [];
function getOpenQueries() {
var retrieveData = async () => {
try {
var value = await AsyncStorage.getItem("user");
var data = JSON.parse(value);
return data.user._id;
} catch (error) {
alert(error);
}
};
retrieveData().then((user) => {
fetch(URL + "/api/contact/open_queries", {
method: "POST",
body: "user=" + user + "&status=open",
headers: { "Content-type": "application/x-www-form-urlencoded" },
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
if (responseJson.error === null) {
setOpenQueries(responseJson.open_queries);
seIsLoading(false);
}
});
});
}
getOpenQueries();
openQueries.forEach((query) => {
open_queries.push(
<TouchableOpacity
onPress={() =>
navigation.navigate("Chat", {
id: query._id,
title: query.title,
query: query,
showInput: true,
})
}
>
<View style={styles.inboxItem}>
<Text style={styles.inboxTitle}>{query.title}</Text>
<Text style={styles.inboxSubtext}>
{query.chats[query.chats.length - 1].chat}
</Text>
<View style={styles.lineBreak}></View>
</View>
</TouchableOpacity>
);
});
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Contacts</Text>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.navigate("NewQuery")}
>
<Text style={styles.text}>Start a new query</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate("ClosedQueries")}>
<View style={styles.button}>
<Text style={styles.text}>Closed Queries</Text>
</View>
</TouchableOpacity>
<Text style={styles.subTitle}>Open Queries</Text>
{isLoading ? (
<View style={styles.loader}>
<Code />
</View>
) : (
<ScrollView style={{ paddingTop: 10 }}>
{openQueries.length > 0 ? (
open_queries
) : (
<Text style={styles.noQuery}>No Open Queries found</Text>
)}
</ScrollView>
)}
<ScrollView></ScrollView>
<BottomNavigation navigation={navigation} active={"contact"} />
</SafeAreaView>
);
}
Try this way
export default function ({ navigation }) {
const [openQueries, setOpenQueries] = useState([]);
const [isLoading, seIsLoading] = useState(true);
const [open_queries_views, setOpenQueriesViews] = useState([]);
function getOpenQueries() {
....
}
// Similar to componentDidMount
useEffect(() => {
getOpenQueries();
});
function renderViews(){
const open_queries = [];
openQueries.forEach((query) => {
open_queries.push(
<TouchableOpacity> ... </TouchableOpacity>
);
});
setOpenQueriesViews(open_queries); // set state here to auto reflect on view
}
return (
<SafeAreaView style={styles.container}>
....
<ScrollView style={{ paddingTop: 10 }}>
{open_queries_views.length > 0 ? (
open_queries_views
) : (
<Text style={styles.noQuery}>No Open Queries found</Text>
)}
</ScrollView>
.....
</SafeAreaView>
);
}

React Native Unmount component before navigation goBack()

I have a screen that has content that is updated by and api call. Whenever I leave the screen, I want to unmount the component so that the next time it is rendered again, because the api is called with different data. Right now, if I enter for the first time, everything works fine (activity indicator shows that it is loading and info is display), but if I go back and then enter again to view the info for another id, it shows the previously loaded information and only after a time it reloads (without the activity indicator displaying). I don't mind the delay, but at least I would like the activity indicator to be displayed while loading.
const AllReservationsScreen = ({navigation, route}) => {
const isFocused = useIsFocused();
const [data, setData] = React.useState({
tableHead: ['Car Number', 'Reserved From', 'Expiration Time', 'Reserved By'],
reservations: "",
isLoading: true
});
useEffect(() => {
setTimeout(async() => {
ReservationService.getReservationsByParkingSpot(route.params.parkingSpotId, route.params.token).then(
(response) => {
if (response.status !== 200) {
return;
}
setData({
... data,
isLoading: false,
reservations: convertReservationsToArray(response.data)
});
}
);
}, 1000);
}, [isFocused]);
const convertReservationsToArray = (reservations) => {
let reservationsArray = [];
reservations.map(
(reservation) =>
reservationsArray.push(
[
reservation.registrationPlateNumber,
moment(reservation.startTime).format('MMMM Do YYYY HH:mm'),
moment(reservation.endTime).format('MMMM Do YYYY HH:mm'),
reservation.user
]
)
);
return reservationsArray;
};
return (
<View style={styles.modal}>
<View style={styles.titleView}>
<View style={styles.titleDetails}>
<Icon
name='ios-arrow-back'
size={30}
color='#8ea7f8'
onPress={() => {
setData({
... data,
isLoading: true,
reservations: []
});
navigation.goBack()
}}
/>
</View>
</View>
<Text style={styles.modalTitle}>All Reservations</Text>
<View style={styles.modalForm}>
{
data.isLoading ? (
<View style={{flex:1,justifyContent:'center',alignItems:'center'}}>
<ActivityIndicator size="large"/>
</View>
) : (
<View style={styles.container}>
<Table borderStyle={{borderWidth: 1, borderColor: 'transparent'}} style={{borderRadius: 10}}>
<Row data={data.tableHead} style={styles.headStyle} textStyle={styles.headText}/>
<ScrollView>
{
//console.log(data.reservations)
data.reservations.map(
(reservation, i) => {
if (i % 2 === 0) {
return <Row
key={i}
data={reservation}
style={styles.evenRow}
textStyle={styles.tableText}
/>
} else {
return <Row
key={i}
data={reservation}
style={styles.oddRow}
textStyle={styles.tableText}
/>
}
}
)
}
</ScrollView>
</Table>
</View>
)
}
</View>
</View>
);
};
You can use navigation.pop() instead of the goBack(), they are exactly the same the only difference is that pop removes the current component.
see: https://reactnavigation.org/docs/stack-actions/#pop
And because you use useEffect you should in the beginning of that function set loading to true again! useEffect works like componentDidMount and componentDidUpdate so you want to be loading everytime you're calling the api. Or just use componentDidMount.