AsyncStorage not working properly when saving multiple items - javascript

I have a main screen where you can see multiple items displayed in card form you can access an item by pressing it to see its details, on the details screen I added a bookmark option that save the item using async storage, you can also check the saved items on a different screen called savedItems screen.
The problem is :
when i bookmark an item it get saved properly and i can go to the savedItems screen and find it there, but some times i have to reload the app for the item to appear on the savedItems screen why is that ?
if i book multiple items they all get saved ( on console.log ) but only the last one appears for some reason i never get more then one item displayed on the SavedItems screen
Bellow is snippets of the code used to book mark ( saved an item on its details screen )
Details.js
const DetailScreen = (props) => {
const [saved, setSaved] = useState([]);
const [items, setItems] = useState(props.route.params);
const onSave = (item) => {
const newItems = [...saved, item];
setSaved(newItems);
const items = JSON.stringify(newItems);
SaveItem("saved", items).then((res) => {
console.log("saved", res);
});
};
const goToDetails = () => {
setSaved([]);
props.navigation.navigate("SaveScreen");
};
const { width, height } = Dimensions.get("window");
const { data } = props.route.params; // this returns the data from each article
//ReadItem("saved").then((res) => console.log(res));
return (
<TouchableOpacity
...
onPress={() => {
onSave(data);
}}
>
<MaterialCommunityIcons
name="bookmark"
size={35}
color={colors.shade2}
/>
</TouchableOpacity>
)
SaveScreen.js
export default class Details extends Component {
state = {
saved: [],
};
removeItem = () => {
DeleteItem("saved")
.then((res) => {
this.setState({
saved: [],
});
console.log(res);
})
.catch((e) => console.log(e));
};
componentDidMount = () => {
ReadItem("saved")
.then((res) => {
if (res) {
const saved = JSON.parse(res);
this.setState({
saved: saved,
});
}
})
.catch((e) => console.warn(e));
};
render() {
return (
<View style={styles.container}>
<FlatList
keyExtractor={(item, index) => index.toString()}
data={this.state.saved}
renderItem={({ item }) => {
return (
<TouchableScale
activeScale={0.9}
tension={50}
friction={7}
useNativeDriver
onPress={() =>
this.props.navigation.navigate("DetailScreen", { data: item })
}
>
<Card item={item} />
</TouchableScale>
);
}}
/>
{this.state.saved.length > 0 && (
<TouchableOpacity onPress={this.removeItem} style={styles.button}>
<Text style={styles.save}>Remove Key</Text>
</TouchableOpacity>
)}
</View>
);
}
}
the code used to save data using async storage
Dbhelper.js
import { AsyncStorage } from "react-native";
export const SaveItem = async (key, value) => {
try {
await AsyncStorage.setItem(key, value);
console.log("saved");
} catch (e) {
console.log(e);
}
};
export const ReadItem = async (key) => {
try {
var result = await AsyncStorage.getItem(key);
return result;
} catch (e) {
return e;
}
};
export function MultiRead(key, onResponse, onFailure) {
try {
AsyncStorage.multiGet(key).then((values) => {
let responseMap = new Map();
values.map((result, i, data) => {
let key = data[i][0];
let value = data[i][1];
responseMap.set(key, value);
});
onResponse(responseMap);
});
} catch (error) {
onFailure(error);
}
}
export async function DeleteItem(key) {
try {
await AsyncStorage.removeItem(key);
return true;
} catch (exception) {
return false;
}
}

if i book multiple items they all get saved ( on console.log ) but only the last one appears for some reason i never get more then one item displayed on the SavedItems screen
I think the problem is when you save new item you remove old items. You have to read old items when you mount DetailScreen. Or you can read items inside onSave method. It's more safety. It depends your application architecture
const DetailScreen = (props) => {
const [items, setItems] = useState(props.route.params);
const onSave = (item) => {
let saved = [];
ReadItem("saved")
.then((res) => {
if (res) {
saved = JSON.parse(res);
}
const newItems = [...saved, item];
const items = JSON.stringify(newItems);
SaveItem("saved", items).then((res) => {
console.log("saved", res);
});
})
.catch((e) => console.warn(e));
};
const goToDetails = () => {
setSaved([]);
props.navigation.navigate("SaveScreen");
};
const { width, height } = Dimensions.get("window");
const { data } = props.route.params; // this returns the data from each article
//ReadItem("saved").then((res) => console.log(res));
return (
<TouchableOpacity
...
onPress={() => {
onSave(data);
}}
>
<MaterialCommunityIcons
name="bookmark"
size={35}
color={colors.shade2}
/>
</TouchableOpacity>
)
when i bookmark an item it get saved properly and i can go to the savedItems screen and find it there, but some times i have to reload the app for the item to appear on the savedItems screen why is that ?
I have only one idea why it may happens. In some cases, you don't unmount SaveScreen. When you return to the SaveScreen componentDidMount is not called. Just try to add console.log to componentDidMount and when you reproduce this behavior if console.log is missed it means you SaveScreen wasn't unmounted
To handle all cases you can try to do that:
export default class Details extends Component {
state = {
saved: [],
};
removeItem = () => {
DeleteItem("saved")
.then((res) => {
this.setState({
saved: [],
});
console.log(res);
})
.catch((e) => console.log(e));
};
componentDidMount = () => {
this.readItems();
this.unsubscribe = navigation.addListener('focus', this.readItems);
};
componentWillUnmount() {
this.unsubscribe();
}
readItems = () => {
ReadItem("saved")
.then((res) => {
if (res) {
const saved = JSON.parse(res);
this.setState({
saved: saved,
});
}
})
.catch((e) => console.warn(e));
}
render() { ... }
}

Related

How to implement multiple functions on a single event handler?

I have this simple react native app that on button click it redirects user to a page. I'm saving data in a cache so that if user clicks the store button it stores data and after refresh it sets store data. However, I would like to implement this logic on a single button, not on 2 different buttons as it is now. Can someone explain me how could I achieve this?
export const App = () => {
const [showFirstWeb, setFirstWeb] = useState(false);
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem("#web_Key");
return setFirstWeb(JSON.parse(jsonValue));
} catch (e) {
console.log("error", e);
}
};
useEffect(() => getData, []);
const storeData = async () => {
try {
const jsonValue = JSON.stringify(showFirstWeb);
await AsyncStorage.setItem("#web_Key", jsonValue);
} catch (e) {
console.log("error", e);
}
};
return (
<View>
<View style={styles.buttonStyle}>
<Button onPress={setFirstWeb(!showFirstWeb)}/>
<Button onPress={storeData} title="store"/>
<View>
{showFirstWeb && <MyWebComponent uri="https://www.google.com/" />}
</View>
</View>
);
};
const MyWebComponent = (uri) => {
return <WebView source={uri} />;
};```
export const App = () => {
const [showFirstWeb, setFirstWeb] = useState(false);
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem("#web_Key");
return setFirstWeb(JSON.parse(jsonValue));
} catch (e) {
console.log("error", e);
}
};
// you forgot to call the function here
useEffect(() => getData(), []);
const storeData = async () => {
try {
// get the new value
const newShowFirstWeb = !showFirstWeb
// use the new value
setFirstWeb(newShowFirstWeb)
const jsonValue = JSON.stringify(newShowFirstWeb );
await AsyncStorage.setItem("#web_Key", jsonValue);
} catch (e) {
console.log("error", e);
}
};
return (
<View>
<View style={styles.buttonStyle}>
<Button onPress={storeData} title="store"/>
<View>
{showFirstWeb && <MyWebComponent uri="https://www.google.com/" />}
</View>
</View>
);
};

What is happening here with React?

I am making a request to my API from the client. I set the information in a React state. The weird thing is the following:
When I print the result of the fetch by console I get this:
But when I display to see in detail, specifically the "explore" array, I get that the result is empty:
The explore array is passed to two components by props to show a list, the super weird thing is that one of the components shows the information but the other does not
Fetch
const baseUrl = process.env.BASE_API_URL;
import { marketPlace } from "./mock";
import { requestOptions } from "utils/auxiliaryFunctions";
const getInitialMarketPlaceData = async (username: string) => {
try {
return await fetch(
`${baseUrl}marketplace/home/${username}`,
requestOptions()
)
.then((data) => data.json())
.then((res) => {
console.log(res.data)
if (res.success) {
return res.data;
}
});
} catch (err) {
throw err;
}
};
Components
export default function Marketplace() {
const [marketPlace, setMarketPlace] = useState<any>();
const [coachData, setCoachData] = useState<false | any>();
const [coach, setCoach] = useState<string>();
const { user } = useContext(AuthContext);
useEffect(() => {
if (!marketPlace) {
getInitialMarketPlaceData(user.username).then((data) => {
console.log("data", data);
setMarketPlace(data);
});
}
}, []);
useEffect(() => {
console.log("marketplace", marketPlace);
}, [marketPlace]);
useEffect(() => {
console.log("coachData", coachData);
}, [coachData]);
const changeCoachData = async (coach: string) => {
setCoach(coach);
let res = await getCoachProfile(coach);
setCoachData(res);
};
if (!marketPlace) {
return <AppLoader />;
}
return (
<main className={styles.marketplace}>
{marketPlace && (
// Highlited viene vacĂ­o de la API
<Highlited
highlitedCoaches={/*marketPlace.highlighted*/ marketPlace.explore}
></Highlited>
)}
{marketPlace.favorites.length !== 0 && (
<Favorites
changeCoachData={changeCoachData}
favCoaches={marketPlace.favorites}
></Favorites>
)}
{
<Explore
changeCoachData={changeCoachData}
exploreCoaches={marketPlace.explore}
></Explore>
}
{/*<Opinions
changeCoachData={changeCoachData}
opinions={marketPlace.opinions}
></Opinions>*/}
{coachData && (
<CoachProfileRookieView
coachData={coachData}
coachUsername={coach}
isCoach={false}
isPreview={false}
onClose={() => setCoachData(false)}
/>
)}
</main>
);
}

Not rerender not modified items when using useCallback function

A page list items, 10 by 10, with an infinite scroll.
Each item has a button "add to favorite", which when is pressed called the callback function handleClickFavorite, in order to not rerender items already rendered.
But, when handleClickFavorite is called, "data" are not fresh... If I had "data" dependency to handleClickFavorite = useCallback(async (item) => {...}, [user, data]);, "data" will be fresh, but each time I load more items, all items are rerended (I have a console.log into my Card PureComponent). So, how How can I do to use a fresh "data" in my handleClickFavorite without rerendered all my items please ?
const Home = () => {
const [user, setUser] = useState({ email: null, auth: false, favorites: [] });
const [data, setData] = useState([]);
const [isInfiniteDisabled, setInfiniteDisabled] = useState(false);
const config = useRef({
page: 0,
});
const loadData = (ev) => {
setInfiniteDisabled(true);
config.current.page += 1;
service.findAll(config.current.page).then(res => {
setData([
...data,
...res['items']
]);
});
}
useIonViewWillEnter(() => {
console.log('useIonViewWillEnter');
loadData();
});
const handleClickFavorite = useCallback(async (item) => {
if (user.auth) {
user.favorites.push(item.id);
setUser(user);
const datas = [...data];
for (let k in datas) {
if (datas[k].id === item.id) {
datas[k].rated = !datas[k].rated;
}
}
setData(datas);
} else {
// show login modal
}
}, [user]);
return (
<IonPage>
<IonContent fullscreen>
<IonList>
{data.map((item, index) => {
return <Card key={'card' + item.id} details={item} onClickFavorite={handleClickFavorite} />
})}
</IonList>
<IonInfiniteScroll
onIonInfinite={loadData}
threshold="100px"
disabled={isInfiniteDisabled}
>
<IonInfiniteScrollContent
loadingSpinner="bubbles"
loadingText="Loading more data..."
></IonInfiniteScrollContent>
</IonInfiniteScroll>
</IonContent>
</IonPage>
);
};

How can I get data from firebase for each element in a Flatlist?

I have a flatlist that receives eventIds as data. On each renderItem call I want to find the corresponding event-object to each eventId on firebase and render this data. (E.g. title or image from this event)
This is the function to get a single event from firestore based on the eventId:
export async function getSingleEventDataFireStore(eventId) {
const db = firebase.firestore();
const eventFirestoreDoc = db.collection("events").doc(eventId);
const doc = await eventFirestoreDoc.get();
if (!doc.exists) {
console.log("No such Event!");
} else {
console.log("Event data fetched!");
return await doc.data();
}
}
this is where the flatlist is displayed:
const EventScreen = () => {
const [event, setEvent] = useState([]);
const [eventFetched, setEventFetched] = useState(false);
const eventIds = [{id:'1', eventId: 'jaelfmk130'}, {id:'2', eventId: '1jlk335n1'}]
const renderItem = ({ item }) => {
if (!eventFetched) {
firestore.getSingleEventDataFireStore(item.eventId).then((eventItem) => {
setEvent(eventItem);
setEventFetched(true);
});
}
setEventFetched(false)
return ( <View><Text>{event.title}</Text></View> )
}
return (
<View>
<FlatList
data={eventIds}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
</View> )
}
This ends up in an infinite loop, because I set eventFetched to false again during renderItem. Otherwise, if I remove setEventFetched(false) it only renders one item.
How can I set eventFetched to false after the render of each item without ending up in a loop?

How can I Autogenerate tabs and also the contents dynamically from firebase realtime database using Javascript

Hello I am new to react native and cant figure out this problem please help me I am working in a project .In my project I want all the tabs and tab content to be autogenerated dynamically from my firebase database
I need that all these nodes (Living room, kitchen, bedroom, etc.) to be the names of the tab and all tabs should show its own content directly from firebase like Living room tab will show app_1 and app_2 similarly Kitchen and bedroom will also autogenerate these directly from database
Like this image:
const HorScrollView = () => {
const [homeId, setHomeId] = useState(0);
const [roomList, setRoomList] =useState(["Loading Rooms...."]);
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve,reject) => {
database().ref(`/USERS/${user.uid}/home_id`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
};
const roomListProvider = ()=>{
return new Promise((resolve,reject) => {
database().ref(`/HOMES/${homeId}/rooms`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
}
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
return (
<View style={styles.scrollViewContainer} >
<ScrollView horizontal>
{roomList.map((roomlist) => (
<Pressable key={roomlist}>
<Text style={styles.scrollViewText} >{roomlist}
</Text>
</Pressable>
))}
</ScrollView>
</View>
);
};
There's a UI library called antd which can help you do this. Use a <Table/> tag for this, then as props pass the value of the columns attribute to be the titles that are coming through, and then the dataSource attribute will be the data under those specific columns. Read the documentation for more.
While you can use the <Tabs> component from antd to achieve what you want, your current code has some bugs regarding how it handles user state and the asynchronous calls.
Taking a look at these lines:
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve, reject) => {
database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => {
resolve(snapshot.val());
});
});
};
Here there are two main problems:
You make use of auth().currentUser but this isn't guaranteed to contain the user object that you expect as it may still be resolving with the server (where it will be null) or the user may be signed out (also null).
You incorrectly chain the promise by wrapping it in a Promise constructor (known as the Promise constructor anti-pattern) where the errors of the original promise will never reach the reject handler leading to crashes.
To fix the user state problem, you should make use of onAuthStateChanged and look out for when the user signs in/out/etc.
function useCurrentUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
// returns [firebase.auth.User | null, boolean]
return [user || null, userLoading];
}
// in your component
const [user, userLoading] = useCurrentUser();
To fix the PCAPs, you'd use:
const homeidProvider = () => {
return database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const roomListProvider = () => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
Because these functions don't depend on state changes, you should place them outside your component and pass the relevant arguments into them.
Next, these lines should be inside a useEffect call where error handling and unmounting the component should be handled as appropriate:
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
should be swapped out with:
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
return;
}
let disposed = false;
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
You should also track the status of your component:
Status
Meaning
"loading"
data is loading
"error"
something went wrong
"signed-out"
no user logged in
"ready"
data is ready for display
Rolling this together:
import { Tabs, Spin, Alert, Card } from 'antd';
const { TabPane } = Tabs;
const { Meta } = Card;
function useUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
return [user || null, userLoading];
}
const getUserHomeId = (uid) => {
return database()
.ref(`/USERS/${uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const getHomeRoomData = (homeId) => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
const RoomView = () => {
const [homeId, setHomeId] = useState(0);
const [status, setStatus] = useState("loading")
const [roomList, setRoomList] = useState([]);
const [user, userLoading] = useUser();
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
setStatus("signed-out");
return;
}
let disposed = false;
setStatus("loading");
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
setStatus("ready");
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
setStatus("error");
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
switch (status) {
case "loading":
return <Spin tip="Loading rooms..." />
case "error":
return <Alert
message="Error"
description="An unknown error has occurred"
type="error"
/>
case "signed-out":
return <Alert
message="Error"
description="User is signed out"
type="error"
/>
}
if (roomList.length === 0) {
return <Alert
message="No rooms found"
description="You haven't created any rooms yet"
type="info"
/>
}
return (
<Tabs defaultActiveKey={roomList[0].key}>
{
roomList.map(room => {
let tabContent;
if (room.apps.length == 0) {
tabContent = "No apps found in this room";
} else {
tabContent = room.apps.map(app => {
<Card style={{ width: 300, marginTop: 16 }} key={app.key}>
<Meta
avatar={
<Avatar src="https://via.placeholder.com/300x300?text=Icon" />
}
title={app.name}
description={app.description}
/>
</Card>
});
}
return <TabPane tab={room.title} key={room.key}>
{tabContent}
</TabPane>
})
}
</Tabs>
);
};

Categories