Render after fetching async data - react-native

I am fetching data in useEffect() and then modifying the object (modifying clients.unreadMessages based on what should render icon), which is later sent to the component to render. But this component does not render correctly always, the icon is sometimes missing. I think it's because data are modified after rendering.
ClientList
import Colors from '#helper/Colors';
import { useSelector } from '#redux/index';
import { HealthierLifeOption } from '#redux/types';
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
import ClientItem from './ClientItem';
import {usePubNub} from "pubnub-react";
export type Client = {
id: number;
username: string;
individualPlanPaid: boolean;
healthierLifeOption?: HealthierLifeOption;
company?: {
id: number;
companyName: string;
};
mentor?: {
id: number;
username: string;
};
}
type Props = {
navigation: any;
clients: Client[];
isAdmin?: boolean;
isLoading: boolean;
onEndReached: Function;
fromAdminTab?: boolean
}
const ClientList = ({ navigation, clients, onEndReached, isLoading, isAdmin = false, fromAdminTab= false }: Props) => {
const resultCount = useSelector(state => state.user.menteesResultCount);
const [hasMoreResults, setHasMoreResults] = useState(true);
const userId = useSelector(state => state.user.id);
const pubnub = usePubNub();
useEffect(() => {
setHasMoreResults(resultCount !== clients?.length);
}, [resultCount, clients]);
useEffect(() => {
getUnreadMessagesProccess().then(r => console.log("aaaaaaaaa"));
}, []);
async function setGrant() {
return new Promise( async resolve => {
await pubnub.grant({
channels: [userId.toString() + '.*'],
ttl: 55,
read: true,
write: true,
update: true,
get: true,
}, response => resolve(response));
});
}
async function getMetadata() {
const options = {include: {customFields: true}};
return await pubnub.objects.getAllChannelMetadata(options);
}
function setChannelsAndTokens(channelMetadata, channels, tokens) {
channelMetadata.data.forEach((value, index) => {
tokens.push(value.custom.lastToken);
channels.push(value.id)
});
}
async function getUnreadedMessages(channels, tokens) {
return await pubnub.messageCounts({
channels: channels,
channelTimetokens: tokens,
});
}
async function getUnreadMessagesProccess() {
const tokens = ['1000'];
const channels = ['1234567'];
const auth = await setGrant();
const channelMetadata = await getMetadata();
const l = await setChannelsAndTokens(channelMetadata, channels, tokens);
const unread = await getUnreadedMessages(channels, tokens).then((res) => {
clients.forEach((value, index) => {
if (res.channels[value.id + '-' + userId + '-chat']) {
value.unreadMessages = res["channels"][value.id + '-' + userId + '-chat'];
} else {
value.unreadMessages = 0;
}
})
console.log(res);
});
return unread;
}
return (
<View>
<FlatList
keyExtractor={item => item.id.toString()}
data={clients}
onEndReached={() => hasMoreResults ? onEndReached() : null}
onEndReachedThreshold={0.4}
renderItem={item => (
<ClientItem
client={item.item}
isAdmin={isAdmin}
navigation={navigation}
fromAdminTab={fromAdminTab}
/>
)}
ListFooterComponent={isLoading
? () => (
<View style={{ padding: 20 }}>
<ActivityIndicator animating size="large" color={Colors.border_gray} />
</View>
)
: null
}
/>
</View>
);
}
export default ClientList;
ClientItem
import React, {useEffect, useState} from 'react';
import { Text, View, Image, TouchableOpacity } from 'react-native';
import Images from '#helper/Images';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import Colors from '#helper/Colors';
import styles from '../styles';
import ClientModal from './ClientModal/ClientModal';
import { Client } from './ClientList';
import { HealthierLifeOption } from '#redux/types';
type Props = {
client: Client;
navigation: any;
isAdmin?: boolean;
fromAdminTab?: boolean
}
const ClientItem = ({ client, navigation, isAdmin = false, fromAdminTab= false }: Props) => {
const [showModal, setShowModal] = useState(false);
const [showIcon, setShowIcon] = useState(false)
console.log(client.unreadMessages)
useEffect(() =>{
if(client.unreadMessages>0)
setShowIcon(true);
},[client.unreadMessages]);
let clientIcon = Images.icon.logoIcon;
const handleContinueButton = () => {
if(!fromAdminTab) {
navigation.navigate('ChatFromClientsList', { selectedId: client.id, showBackButton: true });
}else {
setShowModal(!showModal)
}
};
return (
<View style={styles.client_item_container}>
<TouchableOpacity style={styles.client_content_container} onPress={handleContinueButton}
>
<View style={styles.image_container}>
<Image
style={styles.client_profile_img}
source={clientIcon}
resizeMode="contain"
resizeMethod="resize"
/>
</View>
<View style={styles.title_container}>
<Text style={styles.title} >{ client.username } </Text>
</View>
<View style={styles.dot_container}>
{showIcon && <FontAwesome5
name="comment-dots"
size={20}
color={Colors.red}
/> }
</View>
<View style={styles.hamburger_container} >
<FontAwesome5
name="bars"
size={30}
color={Colors.black}
onPress={() => setShowModal(!showModal)}
/>
</View>
<View>
<ClientModal
isVisible={showModal}
onCollapse={(value: boolean) => setShowModal(value)}
closeModal={(value: boolean) => setShowModal(value)}
client={client}
navigation={navigation}
isAdmin={isAdmin}
/>
</View>
</TouchableOpacity>
</View>
);
};
export default ClientItem;
This code does not render correctly always:
{showIcon && <FontAwesome5
name="comment-dots"
size={20}
color={Colors.red}
/> }

You should not calculate the value in renderItem.
Why you don’t calc the value outside of renderItem in pass it in
useEffect(() =>{
if(client.unreadMessages>0)
setShowIcon(true);
},[client.unreadMessages]);
Do something like:
renderItem={item => (
<ClientItem
client={item.item}
isAdmin={isAdmin}
navigation={navigation}
fromAdminTab={fromAdminTab}
showIcon={item.item.unreadMessages>0}
/>
)}
By the way renderItem should not be a anonymous function.

Related

React Native useEffect API Call

I'm working on an App, which has a search function / page. Now I want some information (an array of meals, not that important) to be loaded when the page opens and then via props given to the subpages from the root search site.
import { Div, Text, Button, Input } from "react-native-magnus";
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import searchFoodInformation from "../utils/searchFoodInformation";
import { SafeAreaView, View, FlatList, StyleSheet, StatusBar, TouchableOpacity } from 'react-native';
import BottomSheet from "react-native-gesture-bottom-sheet";
import FoodInformationModal from "./FoodInformationModal";
import { Dimensions } from 'react-native';
import mealsByDay from "../utils/mealsByDayAndUser";
const Search = ({navigation, route}) => {
const bottomSheet = useRef();
const [searchString, setSearchString] = useState("")
const [error, setError] = useState('');
const [results, setResults] = useState("")
const [clickedInfo, setClickedInfo] = useState({})
const [meals, setMeals] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
(async () => {
try {
const mealsByDayRes = await mealsByDay("10-01-2022");
if (mealsByDayRes != null) {
setMeals(mealsByDayRes);
}
} catch (error) {
console.log(error);
}
setLoading(false);
})();
}, [])
const SearchForFoodInformation = () => {
setError("")
setResults("")
searchFoodInformation(searchString).then((res) => {
try {
if (res.searchFoodInformation.error != "") {
setError(res.searchFoodInformation.error)
setResults("")
} else {
setError("")
setResults(res.searchFoodInformation.foodInformation)
}
} catch (error) {
alert(error)
}
}).catch(error => {setError(error)})
}
try {
if (route.params.params) {
setSearchString(route.params.params.data)
route.params.params = null
SearchForFoodInformation()
}
} catch (e) {
}
const Item = ({ element }) => (
<TouchableOpacity
onPress={() => {
setClickedInfo(element)
bottomSheet.current.show()
}}
>
<View style={styles.item}>
<Text>{element.name}</Text>
</View>
</TouchableOpacity>
);
const renderItem = ({ item }) => (
<Item element={item} />
);
return (
<>
{}
{!loading ? (
meals != null ? (
<Div shadow="sm" rounded="md" alignItems="center" mt={50} >
{error.length > 0 ? <Text color="black" p="md" borderStyle="solid" borderColor="red" borderWidth={3} rounded={16} m={10} bg="red"> {error} </Text> : null }
<Div alignItems="center" row>
<Input icon="ios-search" placeholder="Search" onChangeText={(text) => setSearchString(text)} value={searchString} mr="10%" ml="1%" />
<Button onPress={() => {SearchForFoodInformation()}} alignSelf="center" rounded="md" mr={"1%"} ml="auto">Suchen</Button>
</Div>
{ results.length > 0 &&
<Div pt={50}>
<FlatList
data={results}
renderItem={renderItem}
/>
</Div>
}
<BottomSheet hasDraggableIcon ref={bottomSheet} height={Dimensions.get("screen").height * 0.9}>
<FoodInformationModal id={clickedInfo.id} name={clickedInfo.name} nutrients={clickedInfo.nutrients} description={clickedInfo.description} meals={meals}/>
</BottomSheet>
</Div>
) : (
<Text>Loading...</Text>
)) : (
<Text>Loading...</Text>
)
}
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
},
item: {
backgroundColor: '#eee',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
}
})
export default Search
But the call for mealsByDay isn't executed and so the value is always undefined, but the site renders like the values is not null.
Thanks

invalid hook call in mobx+react native

I'm new to mobx,
I was told that I can't use directly rootStore from rootStore.tsx directly, and I have to replace it with hook, so I've tried to call hook useStore from rootStore.tsx
but in this case I've got an error "invalid hook call. Hooks can be called inside of the body"
my files are:
rootStore.tsx
import { createContext, useContext } from 'react'
import { makeAutoObservable } from 'mobx'
import { AsyncTrunk } from 'mobx-sync'
import AsyncStorage from '#react-native-async-storage/async-storage'
import { DayStyle, firstDayStyle } from '../styles/markedDayStyle'
const period: Record<string, DayStyle> = {
'2022-02-16': firstDayStyle,
}
export const rootStore = makeAutoObservable({
periods: period,
})
export const trunk = new AsyncTrunk(rootStore, {
storage: AsyncStorage,
})
export const StoreContext = createContext(rootStore)
export const StoreProvider = StoreContext.Provider
export const useStore = () => useContext(StoreContext)
App.tsx
const App = observer(({}) => {
const store = useStore()
const [isStoreLoaded, setIsStoreLoaded] = useState(false)
useEffect(() => {
const rehydrate = async () => {
await trunk.init()
setIsStoreLoaded(true)
}
rehydrate().catch(() => console.log('problems with localStorage'))
}, [store])
if (!isStoreLoaded) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" />
</View>
)
} else {
return (
<StoreProvider value={store}>
<PaperProvider theme={store.isDarkMode ? darkTheme : lightTheme}>
<View style={styles.container}>
<CalendarScreen/>
</View>
</PaperProvider>
</StoreProvider>
)
}
})
CalendarScreen.tsx
const CalendarScreen = observer(({}) => {
const store = useStore()
const handleDayPress = (day: DateData) => {
setModalVisible(true)
setPressedDay(day.dateString)
}
return (
<SafeAreaView style={styles.screenContainer}>
<Calendar
onDayPress={day => {handleDayPress(day)}}
/>
<View>
<ModalConfirmDay modalVisible={modalVisible} setModalVisible={setModalVisible} pressedDay={pressedDay} />
</View>
</SafeAreaView>
)
)}
ModalConfirmDay.tsx
import { fillMarkedDays } from '../functions/fillMarkedDays'
const ModalConfirmDay = observer(({ modalVisible, setModalVisible, pressedDay }: ModalConfirmDayProps) => {
const handlePeriodStarts = () => {
fillMarkedDays(pressedDay)
setModalVisible(false)
}
return (
<View style={styles.centeredView}>
<Modal
visible={modalVisible}
>
<View style={styles.modalView}>
<TouchableOpacity onPress={() => handlePeriodStarts()}>
<Text>Period starts</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
)
})
fillMarkedDays.tsx
import { rootStore, useStore} from '../store/rootStore'
import { firstDayStyle} from '../styles/markedDayStyle'
const fillMarkedDays = (selectedDay: string) => {
const store = useStore()
if (selectedDay) {
store.periods[selectedDay] = firstDayStyle
}
}
when I try to add a new key-value (in fillMarkedDays.tsx) to store.periods I'm getting this
how can I fix this or select a better approach to call the store? Thanks everyone
By the rules of hooks you can't use hooks outside of the body of the function (component), so basically you can only use them before return statement, and also you can't use any conditions and so on. fillMarkedDays is just a function, not a component, it has no access to React context, hooks or whatever.
What you can do is first get the store with hook, then pass it as an argument into the fillMarkedDays function:
const ModalConfirmDay = observer(({ modalVisible, setModalVisible, pressedDay }: ModalConfirmDayProps) => {
const store = useStore()
const handlePeriodStarts = () => {
fillMarkedDays(store, pressedDay)
setModalVisible(false)
}
// ...
}

Why can't I navigate to my Screen using onPress={() => navigation.navigate('SaveProfileImage, { profileImage })}?

I'm developing an app where I have a screen called Add, other called Save, that I navigate between them using onPress={() => navigation.navigate('Save', { image })} inside a button, so I tried to do the same on another screen called profile that should navigate to the screen SaveProfileImage, but when I try this, I get the error TypeError: undefined is not an object (evaluating 'props.route.params') and I can't understand what can be so wrong in my codes:
Here is the Profile codes:
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, Image, FlatList, Button } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import firebase from 'firebase';
require('firebase/firestore');
import { connect } from 'react-redux';
function Profile({ navigation }, props) {
const [userPosts, setUserPosts] = useState([]);
const [user, setUser] = useState(null);
const [hasGalleryPermission, setHasGalleryPermission] = useState(null);
const [image, setImage] = useState(null);
useEffect(() => {
const { currentUser, posts } = props;
if (props.route.params.uid === firebase.auth().currentUser.uid) {
setUser(currentUser)
setUserPosts(posts)
}
else {
firebase.firestore()
.collection('users')
.doc(props.route.params.uid)
.get()
.then((snapshot) => {
if (snapshot.exists) {
setUser(snapshot.data());
}
else {
console.log('does not exist')
}
})
firebase.firestore()
.collection('posts')
.doc(props.route.params.uid)
.collection('userPosts')
.orderBy('creation', 'desc')
.get()
.then((snapshot) => {
let posts = snapshot.docs.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data }
})
setUserPosts(posts)
});
};
(async () => {
const galleryStatus = await ImagePicker.requestMediaLibraryPermissionsAsync();
setHasGalleryPermission(galleryStatus.status === 'granted');
})();
}, [props.route.params.uid, props.following]);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.cancelled) {
setImage(result.uri);
};
};
if (hasGalleryPermission === false) {
return <View />;
};
if (hasGalleryPermission === false) {
return <Text>No access to gallery</Text>;
};
const onLogout = () => {
firebase.auth().signOut();
};
if (user === null) {
return <View />
}
return (
<View style={styles.container}>
{image && <Image source={{ uri: image }}
style={{ flex: 1 }} />}
<Image source={{ uri: props.route.params.profileImage }} />
<View style={styles.containerInfo}>
<Text>{user.name}</Text>
<Text>{user.state}</Text>
<Text>{user.city}</Text>
<Text>{user.email}</Text>
{props.route.params.uid !== firebase.auth().currentUser.uid}
<Button title='Pick Image From Gallery' onPress={() => pickImage()} />
<Button title='Save'
onPress={() => navigation.navigate('SaveProfileImage',
{ profileImage })} />
</View>
<View style={styles.container}>
<FlatList
numColumns={3}
horizontal={false}
data={userPosts}
renderItem={({ item }) => (
<View
style={styles.containerImage}>
<Image
style={styles.image}
source={{ uri: item.downloadURL }}
/>
</View>
)}
/>
</View>
<Button
title='Logout'
onPress={() => onLogout()}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
containerInfo: {
margin: 20
},
containerImage: {
flex: 1 / 3
},
image: {
flex: 1,
aspectRatio: 1 / 1
}
})
const mapStateToProps = (store) => ({
currentUser: store.userState.currentUser,
posts: store.userState.posts,
following: store.userState.following
})
export default connect(mapStateToProps, null)(Profile);
The app.js has a navigation container to control these screens:
<NavigationContainer >
<Stack.Navigator initialRouteName='Main'>
<Stack.Screen name='Main' component={MainScreen} />
<Stack.Screen name='Add' component={AddScreen}
navigation={this.props.navigation}/>
<Stack.Screen name='Save' component={SaveScreen}
navigation={this.props.navigation}/>
<Stack.Screen name='SaveProfileImage' component={SaveProfileImageScreen}
navigation={this.props.navigation}/>
<Stack.Screen name='Comment' component={CommentScreen}
navigation={this.props.navigation}/> </Stack.Navigator>
<NavigationContainer >
I can't understand why I can navigate from the AddScreen to the Save Screen, but I can't do the same between the profileScreen and the SaveProfileImage.
The save is this one:
import React, { useState } from 'react'
import { View, TextInput, Image, Button } from 'react-native'
import firebase from 'firebase'
require('firebase/firestore')
require('firebase/firebase-storage')
export default function Save(props) {
const [caption, setCaption] = useState('')
const uploadImage = async () => {
const uri = props.route.params.image;
const childPath =
`post/${firebase.auth().currentUser.uid}/${Math.random().toString(36)}`;
const response = await fetch(uri);
const blob = await response.blob();
const task = firebase
.storage()
.ref()
.child(childPath)
.put(blob);
const taskProgress = snapshot => {
console.log(`transferred: ${snapshot.bytesTransferred}`)
};
const taskCompleted = () => {
task.snapshot.ref.getDownloadURL().then((snapshot) => {
savePostData(snapshot);
})
};
const taskError = snapshot => {
console.log(snapshot)
};
task.on('state_changed', taskProgress, taskError, taskCompleted);
};
const savePostData = (downloadURL) => {
firebase.firestore()
.collection('posts')
.doc(firebase.auth().currentUser.uid)
.collection('userPosts')
.add({
downloadURL,
caption,
likesCount: 0,
estate: '',
city: '',
creation: firebase.firestore.FieldValue.serverTimestamp()
}).then((function () {
props.navigation.popToTop()
}))
};
return (
<View style={{ flex: 1 }}>
<Image source={{ uri: props.route.params.image }} />
<TextInput
placeholder='Write a Caption . . .'
onChangeText={(caption) => setCaption(caption)}
/>
<Button title='Save' onPress={() => uploadImage()} />
</View>
);
};
The SaveProfileScreen is this other one:
import React, { useState } from 'react'
import { View, TextInput, Image, Button } from 'react-native'
import firebase from 'firebase'
require('firebase/firestore')
require('firebase/firebase-storage')
export default function ProfileImage(props) {
const [caption, setCaption] = useState('')
const uploadImage = async () => {
const uri = props.route.params.image;
const childPath =
`profileImage/${firebase.auth().currentUser.uid}/
${Math.random().toString(36)}`;
console.log(childPath);
const response = await fetch(uri);
const blob = await response.blob();
const task = firebase
.storage()
.ref()
.child(childPath)
.put(blob);
const taskProgress = snapshot => {
console.log(`transferred: ${snapshot.bytesTransferred}`)
};
const taskError = snapshot => {
console.log(snapshot)
};
const taskCompleted = () => {
task.snapshot.ref.getDownloadURL().then((snapshot) => {
savePictureData(snapshot);
})
};
task.on('state_changed', taskProgress, taskError, taskCompleted);
};
return (
<View style={{ flex: 1 }}>
<Image source={{ uri: props.route.params.profileImage }} />
<Button title='Save' onPress={() => uploadImage()} />
</View>
);
};

React Native SectionList not reliably rendering one of its sections

I have a custom SectionList component, called EventSectionList:
import React from 'react';
import { SectionList } from 'react-native';
import { View, Text, Fab } from '../../../common/Layout';
import EventCard from '../EventCard';
import styles from './styles';
const EventSectionList = ({
sections,
refreshing,
onRefresh,
onEndReached,
onFabPress,
inExtraData,
}) => {
const renderSectionHeader = ({ section: { title } }) => (
<Text style={styles.sectionHeader}>{title}</Text>
);
const renderSectionFooter = () => <View style={styles.sectionSeparator} />;
const renderSeparator = () => <View style={styles.itemSeparator} />;
const renderFooter = () => <View style={styles.footer} />;
const renderEvent = event => {
const { item } = event;
return <EventCard event={item} />;
};
return (
<View style={styles.container}>
<SectionList
sections={sections}
refreshing={refreshing}
onRefresh={onRefresh}
onEndReachedThreshold={0}
onEndReached={onEndReached}
renderItem={renderEvent}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
ItemSeparatorComponent={renderSeparator}
ListFooterComponent={renderFooter}
showsVerticalScrollIndicator={false}
extraData={inExtraData}
keyExtractor={(item, index) => String(index)}
/>
<Fab icon="add" style={styles.fabAdd} onPress={onFabPress} />
</View>
);
};
export default EventSectionList;
An EventSectionList is being rendered here on the EventsScreen:
import React, { useState, useEffect, Fragment } from 'react';
import { isEmpty } from 'lodash';
import { Text } from '../../../common/Layout';
import { connect } from './../../../common/Helpers';
import EventSectionList from './../../Events/EventSectionList';
import styles from './styles';
const EventsScreen = ({ services }) => {
const { eventsService } = services;
const [hostedEvents, setHostedEvents] = useState([]);
const [registeredEvents, setRegisteredEvents] = useState([]);
const [eventSections, setEventSections] = useState(null);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
useEffect(() => {
if (!isEmpty(eventSections)) {
setRefreshing(false);
}
}, [eventSections]);
useEffect(() => {
if (!isEmpty(hostedEvents) || !isEmpty(registeredEvents)) {
setEventSections([
{
title: 'Upcoming Hosted Events',
data: hostedEvents,
},
{
title: 'Upcoming Registered Events',
data: registeredEvents,
},
]);
}
}, [hostedEvents, registeredEvents]);
useEffect(() => {
const hostedEventsPromise = async () => {
const { data } = await eventsService.getEvents({
page,
hosted: true,
upcoming: true,
page_size: 100,
});
if (page === 1) {
setHostedEvents(data);
} else {
setHostedEvents([...hostedEvents, ...data]);
}
};
const registeredEventsPromise = async () => {
const { data } = await eventsService.getEvents({
page,
hosted: false,
upcoming: true,
page_size: 100,
});
if (page === 1) {
setRegisteredEvents(data);
} else {
setRegisteredEvents([...registeredEvents, ...data]);
}
};
registeredEventsPromise();
hostedEventsPromise();
}, [page]);
const eventsRefresh = async () => {
if (!refreshing) {
setPage(1);
}
};
const eventsRefreshEnd = async () => {
if (!refreshing) {
setPage(page + 1);
}
};
return (
<Fragment>
{eventSections ? (
<EventSectionList
sections={eventSections}
inExtraData={registeredEvents}
refreshing={refreshing}
onRefresh={eventsRefresh}
onEndReached={eventsRefreshEnd}
onFabPress={() => {}}
/>
) : (
<Text style={styles.message}>You have no upcoming events.</Text>
)}
</Fragment>
);
};
export default connect()(EventsScreen);
Unfortunately, sometimes - the Registered Events section doesn't render, until the pull - down refresh occurs, and even then - it doesn't always render. I can see this section sort of trying to render - the EventCards flicker where they should be rendered, but they don't "stick".
For reference, here is the EventCard component:
import React, { useState } from 'react';
import { isEmpty } from 'lodash';
import moment from 'moment';
import { connect } from '../../../common/Helpers';
import { View, Text, Touchable } from '../../../common/Layout';
import { Image } from '../../../common/Image';
import styles from './styles';
const EventCard = ({ event }) => {
const dateFormat = 'MMM D';
const timeFormat = 'h:mm A';
const [startDate] = useState(
moment(event.start_date)
.format(dateFormat)
.toUpperCase()
);
const [startTime] = useState(moment(event.start_date).format(timeFormat));
const [startDateTime] = useState(`${startDate} AT ${startTime}`);
const [name] = useState(event.name);
const [address] = useState(
event.start_location && event.start_location.address
);
const [uri] = useState(event.cover_image);
return (
<Touchable>
<View style={styles.container}>
{!isEmpty(uri) ? (
<Image source={{ uri }} style={styles.detailsImage} />
) : (
<Image name="eventPlaceholder" style={styles.detailsImage} />
)}
<View style={styles.detailsContainer}>
<Text style={styles.detailsDate}>{startDateTime}</Text>
<Text style={styles.detailsName}>{name}</Text>
<Text style={styles.detailsAddress}>{address}</Text>
</View>
</View>
</Touchable>
);
};
export default connect()(EventCard);

Saving list with AsyncStorage

So I made a "notepad" app and I want to do so the text that the user wrote etc it should be saved, so the text doesn't get reset when user quits the app.
I'm new to react-native, after a few google searches I need AsyncStorage? to make this happen.
but really dunno on how to do it.
import React, { useState } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
TouchableWithoutFeedback,
TouchableOpacity,
Keyboard,
AsyncStorage
} from 'react-native';
import Header from './components/header';
import ListItem from './components/listitem';
import AddList from './components/addlist';
export default function App() {
const [todos, setTodos] = useState([
]);
const pressHandler = (key) => {
setTodos((prevTodos) => {
return prevTodos.filter(todo => todo.key != key);
});
}
const submitHandler = (text) => {
if(text.length > 0) {
setTodos((prevTodos) => {
return [
{ text: text, key: Math.random().toString() },
...prevTodos
];
})
}
}
return (
<TouchableWithoutFeedback onPress={() => {
Keyboard.dismiss();
}}>
<View style={styles.container}>
<Header />
<View style={styles.content}>
<AddList submitHandler={submitHandler} />
<View style={styles.todoList}>
<FlatList
data={todos}
renderItem={({ item }) => (
<ListItem item={item} pressHandler={pressHandler} />
)}
/>
</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}
new problem out of nowhere worked great before now broken without touching the code
const pressHandler = key =>
setTodos(prevTodos => {
const newTodos = prevTodos.filter(todo => todo.key !== key);
storeTodosInAsync(newTodos);
console.log(prevTodos);
return newTodos;
});
const submitHandler = text => {
if (text.length > 0) {
const key = Math.random().toString();
setTodos(prevTodos => {
const newTodos = [{ text, key }, ...prevTodos];
storeTodosInAsync(newTodos);
console.log(newTodos);
return newTodos;
});
}
};
You can use AsyncStorage to store and load data to/from local storage. One thing to note is data MUST be a string, so anything like an object that is not a string needs to be stringified. You can use JSON.stringify(...) to do this. And then when you get the string back you can use JSON.parse(...) to convert it back into an object.
So to convert your current code into something that automatically loads saved todos and always saves the latest, you could write this:
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
TouchableWithoutFeedback,
TouchableOpacity,
Keyboard,
AsyncStorage,
Button
} from 'react-native';
import Header from './components/header';
import ListItem from './components/listitem';
import AddList from './components/addlist';
export default function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
restoreTodosFromAsync();
}, []);
const pressHandler = key => {
console.log('Todos BEFORE delete');
console.log(todos);
const newTodos = todos.filter(todo => todo.key !== key);
console.log('Todos AFTER delete');
console.log(todos);
setTodos(newTodos);
storeTodosInAsync(newTodos);
};
const submitHandler = text => {
if (text.length === 0) return;
const key = Math.random().toString();
console.log('Todos BEFORE submit');
console.log(todos);
const newTodos = [{ text, key }, ...todos];
console.log('Todos AFTER submit');
console.log(todos);
setTodos(newTodos);
storeTodosInAsync(newTodos);
};
const asyncStorageKey = '#todos';
const storeTodosInAsync = newTodos => {
const stringifiedTodos = JSON.stringify(newTodos);
AsyncStorage.setItem(asyncStorageKey, stringifiedTodos).catch(err => {
console.warn('Error storing todos in Async');
console.warn(err);
});
};
const restoreTodosFromAsync = () => {
AsyncStorage.getItem(asyncStorageKey)
.then(stringifiedTodos => {
console.log('Restored Todos:');
console.log(stringifiedTodos);
const parsedTodos = JSON.parse(stringifiedTodos);
if (!parsedTodos || typeof parsedTodos !== 'object') return;
setTodos(parsedTodos);
})
.catch(err => {
console.warn('Error restoring todos from async');
console.warn(err);
});
};
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<Header />
<View style={styles.content}>
<AddList submitHandler={submitHandler} />
<View style={styles.todoList}>
<FlatList
data={todos}
renderItem={({ item }) => <ListItem item={item} pressHandler={pressHandler} />}
/>
</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}