I'm trying to build a react-native app using Expo, a simple movie app with OMDB api. Having some problems with AsyncStorage...
Firstly it doesn't seem to save properly... usually it works but then when i completely close the expo app and reload the data doesn't exist again...
and secondly i have a 'poster' component that is shared for displaying movies from a search query and also for movies in my watched list. The button text is supposed to change from 'add' to 'remove' if the object already exists in the watched list.
For some reason it works on the watched list, but not on the search query list.
Heres App.tsx:
interface TVPROPS {
Poster: string;
Title: string;
Type: string;
Year: string;
imdbID: string;
}
export default function App() {
const [search, setSearch] = useState<string>("");
const [tvData, setTVData] = useState([]);
const [watched, setWatched] = useState<TVPROPS[]>([]);
const [watchClick, setWatchClick] = useState<Boolean>(false);
const saveToLocalStorage = async (items: TVPROPS[]) => {
await AsyncStorage.setItem("react-watched", JSON.stringify(items));
};
const getLocalStorage = async () => {
const movieFavourites = await AsyncStorage.getItem("react-watched");
if (movieFavourites !== null) {
const movieFavouritesParse: TVPROPS[] = JSON.parse(movieFavourites);
setWatched(movieFavouritesParse);
}
};
const handleSearch = async (searchInput: string) => {
const response = await axios(
`https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchInput}`
);
const data = response.data;
setTVData(data.Search);
};
useEffect(() => {
getLocalStorage();
}, []);
useEffect(() => {
saveToLocalStorage(watched);
}, [watched]);
return (
<>
<View style={{ backgroundColor: "grey" }}>
<View style={{ marginTop: 40 }}>
<View style={{ flexDirection: "row", padding: 10 }}>
<View
style={{
width: "50%",
borderBottomWidth: watchClick ? 0 : 1,
padding: 10,
borderColor: "white",
}}
>
<Pressable onPress={() => setWatchClick(false)}>
<Text
style={{
textAlign: "center",
color: watchClick ? "black" : "white",
}}
>
Search
</Text>
</Pressable>
</View>
<View
style={{
width: "50%",
borderBottomWidth: watchClick ? 1 : 0,
padding: 10,
borderColor: "white",
}}
>
<Pressable onPress={() => setWatchClick(true)}>
<Text
style={{
textAlign: "center",
color: watchClick ? "white" : "black",
}}
>
Watched
</Text>
</Pressable>
</View>
</View>
<TextInput
style={{ textAlign: "center" }}
onChangeText={(text) => setSearch(text)}
placeholder="Film or TV Show"
></TextInput>
<Button onPress={() => handleSearch(search)} title="Search" />
</View>
</View>
{watchClick ? (
<ScrollView
contentContainerStyle={{
flexDirection: "row",
flexWrap: "wrap",
}}
>
{watched
?.filter((w) => w.Title.includes(search))
.map((data: TVPROPS, index: number) => (
<Poster
key={index}
data={data}
watched={watched}
setWatched={setWatched}
/>
))}
</ScrollView>
) : (
<ScrollView
contentContainerStyle={{
flexDirection: "row",
flexWrap: "wrap",
}}
>
{tvData?.map((data: TVPROPS, index: number) => (
<Poster
key={index}
data={data}
watched={watched}
setWatched={setWatched}
/>
))}
</ScrollView>
)}
</>
);
}
and heres Poster.tsx:
type Props = {
data: TVPROPS;
watched: TVPROPS[];
setWatched: any;
};
const Poster = ({ data, watched, setWatched }: Props) => {
const [modalVisible, setModalVisible] = useState(false);
const [TVInfo, setTVInfo] = useState<any>();
const handleClick = async () => {
const response = await axios(
`https://www.omdbapi.com/?apikey=${API_KEY}&i=${data.imdbID}`
);
const info = response.data;
setTVInfo(info);
setModalVisible(true);
};
const handleWatched = async () => {
if (watched.includes(data)) {
setWatched(
watched.filter((favourite) => favourite.imdbID !== data.imdbID)
);
await AsyncStorage.setItem("react-watched", JSON.stringify(watched));
} else {
setWatched([data, ...watched]);
await AsyncStorage.setItem("react-watched", JSON.stringify(watched));
}
};
return (
<>
<Pressable
style={{
width: "32%",
margin: 2,
backgroundColor: "slategrey",
borderRadius: 10,
padding: 2,
}}
onPress={() => handleClick()}
>
<View>
<Text style={{ textAlign: "center", color: "white" }}>
{data?.Year}
</Text>
<Image
source={{ uri: data?.Poster }}
style={{
width: 100,
height: 150,
alignSelf: "center",
margin: 2,
}}
/>
<Text style={{ textAlign: "center", color: "white" }}>
{data?.Title}
</Text>
<View style={{ alignSelf: "baseline", width: "100%" }}>
<Button
title={watched.includes(data) ? "Remove" : "Add"}
onPress={() => handleWatched()}
></Button>
</View>
</View>
</Pressable>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}
>
<View
style={{
borderRadius: 10,
marginTop: "25%",
padding: 10,
backgroundColor: "white",
margin: 20,
alignItems: "center",
}}
>
<Text style={{ fontSize: 18, textAlign: "center" }}>
{TVInfo?.Title}
</Text>
<Image
source={{ uri: TVInfo?.Poster }}
style={{
width: 200,
height: 300,
alignSelf: "center",
}}
/>
<Text>Year: {TVInfo?.Year}</Text>
<Text>Rating: {TVInfo?.Rated}</Text>
<Text>Genre: {TVInfo?.Genre}</Text>
<Text>Language: {TVInfo?.Language}</Text>
<Text style={{ textAlign: "center" }}>{TVInfo?.Plot}</Text>
<Text>Metascore: {TVInfo?.Metascore}</Text>
<Pressable
onPress={() => setModalVisible(!modalVisible)}
>
<Text>
Close
</Text>
</Pressable>
</View>
</Modal>
</>
);
};
export default Poster;
CodePudding user response:
It is most likely this:
useEffect(() => {
saveToLocalStorage(watched);
}, [watched]);
When your component mounts for the first time watched
is an empty array. Then your effect runs to retrieve the data from storage and set it. Then the above effect runs, and the original intention was probably to ensure the store stays in sync with watched
.
However, on the first mount, this will overwrite the storage with that initial empty array. This is because whenever you call a set state operation, it doesn't change the value of the state immediately, it queues it for the next render. That means this second effect (which is run inside the same render, including on mount) sees []
as the value of watched
even though the top effect ran before. There's also that getLocalStorage
is asynchronous so that nails down the point that by the time this second effect runs, the data hasn't been set into watched yet. But this code wouldn't work anyway even if it was synchronous, because of the above point about how react sees state between effects within a single render.
For the setting of the storage, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the react docs.
interface TVPROPS {
Poster: string;
Title: string;
Type: string;
Year: string;
imdbID: string;
}
export default function App() {
const [search, setSearch] = useState<string>("");
const [tvData, setTVData] = useState([]);
const [watched, setWatched] = useState<TVPROPS[]>([]);
const [watchClick, setWatchClick] = useState<Boolean>(false);
const saveToLocalStorage = async (items: TVPROPS[]) => {
await AsyncStorage.setItem("react-watched", JSON.stringify(items));
};
const getLocalStorage = async () => {
const movieFavourites = await AsyncStorage.getItem("react-watched");
if (movieFavourites !== null) {
const movieFavouritesParse: TVPROPS[] = JSON.parse(movieFavourites);
setWatched(movieFavouritesParse);
}
};
const handleSearch = async (searchInput: string) => {
const response = await axios(
`https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchInput}`
);
const data = response.data;
setTVData(data.Search);
};
useEffect(() => {
getLocalStorage();
}, []);
const handleSetWatched = (items: TVPROPS[]) => {
setWatched(items);
saveToLocalStorage(items);
}
return (
<>
<View style={{ backgroundColor: "grey" }}>
<View style={{ marginTop: 40 }}>
<View style={{ flexDirection: "row", padding: 10 }}>
<View
style={{
width: "50%",
borderBottomWidth: watchClick ? 0 : 1,
padding: 10,
borderColor: "white",
}}
>
<Pressable onPress={() => setWatchClick(false)}>
<Text
style={{
textAlign: "center",
color: watchClick ? "black" : "white",
}}
>
Search
</Text>
</Pressable>
</View>
<View
style={{
width: "50%",
borderBottomWidth: watchClick ? 1 : 0,
padding: 10,
borderColor: "white",
}}
>
<Pressable onPress={() => setWatchClick(true)}>
<Text
style={{
textAlign: "center",
color: watchClick ? "white" : "black",
}}
>
Watched
</Text>
</Pressable>
</View>
</View>
<TextInput
style={{ textAlign: "center" }}
onChangeText={(text) => setSearch(text)}
placeholder="Film or TV Show"
></TextInput>
<Button onPress={() => handleSearch(search)} title="Search" />
</View>
</View>
{watchClick ? (
<ScrollView
contentContainerStyle={{
flexDirection: "row",
flexWrap: "wrap",
}}
>
{watched
?.filter((w) => w.Title.includes(search))
.map((data: TVPROPS, index: number) => (
<Poster
key={index}
data={data}
watched={watched}
setWatched={handleSetWatched}
/>
))}
</ScrollView>
) : (
<ScrollView
contentContainerStyle={{
flexDirection: "row",
flexWrap: "wrap",
}}
>
{tvData?.map((data: TVPROPS, index: number) => (
<Poster
key={index}
data={data}
watched={watched}
setWatched={handleSetWatched}
/>
))}
</ScrollView>
)}
</>
);
}