Problem with Hook and AsyncStorage to load a FlatList - react-native

I have 2 screens in my App one that has a form where the user stores the data that it fills in AsyncStorage and this screen that reads all the data saved in AsyncStorage and should show the data in a FlatList. The problem here is that nothing is rendered and the screen is blank. I dont know where the problem is located because if you read the code the console.log(productosData) actually returns in my command line exactly the same structure of result. So productosData is loaded without problem but for some reason this doesn't work.
export default function TestScreen () {
const [productosData, setproductosData] = useState([]);
const ItemView = ({item}) => {
return (
<View>
<Text>
{item}
</Text>
</View>
);
};
useEffect( () => {
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
result.forEach(element => (setproductosData(productosData.push(element))));
console.log(productosData);
}
cargarEnEstado()
},[])
return (
<View style={styles.container}>
<FlatList
data={productosData}
renderItem={ItemView}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 2,
justifyContent: 'center',
alignItems: 'center',
}
});
So I maybe thought that the problem was my FlatList and then I decided to take my FlatList out and test the hook with a Text but when I use {productosData} inside the Text the screen shows a number that corresponds with the length of the first array of result. So in this case I see in the screen a 4 because result as well as productosData have this structure [["a","b"],["c","d"],["e","f"],["g","h"]] with the length of the first array being 4.
export default function TestScreen () {
const [productosData, setproductosData] = useState([]);
const ItemView = ({item}) => {
return (
<View>
<Text>
{item}
</Text>
</View>
);
};
useEffect( () => {
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
result.forEach(element => (setproductosData(productosData.push(element))));
console.log(productosData);
}
cargarEnEstado()
},[])
return (
<View style={styles.container}>
<Text> {productosData} </Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 2,
justifyContent: 'center',
alignItems: 'center',
}
});
Any suggestions? Thank you in advance.

The reason nothing is being rendered is because element is an array of strings ['a','b'], if I understood correctly. Try to change your itemView to this
const ItemView = ({item}) => {
return (
<View>
<Text>
{item[0]+ ' , ' + item[1]}
</Text>
</View>
);
};
Also, your useEffect is not very clean. Note that state in React is immutable. By calling push on productosData, you're mutating the state. First, create a shallow copy of your state, then push your new objects into the copy. Only then you would update your state.
However, there is no reason to iterate your results, just spread them on the state, like this:
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
setProductosData([...productosData, ...result])
console.log(productosData); // Also state is async, so this might not log the correct value
}

Related

React-Native: Ref is missing scrollToIndex() for FlatList

I am passing a ref to a FlatList and expecting to access the scrollToIndex() function, but it doesn't appear in the console and it throws an error when I try to use it saying "scrollTo() is not a function", it doesn't even recognise that I'm trying to use scrollToIndex().
This is a simple example of usage in my code:
const ref = useRef()
ref.current?.scrollToIndex({index:itemIndex,animated:true})
<FlatList ref={ref} .../>
Has anyone experienced this and knows how to make it work? I've looked over the documentation and examples and everything seems to be fine. Any help would be appreciated!
UPDATE:
Component:
import React from "react";
import { View, FlatList, ImageBackground,TouchableOpacity,ScrollView,StyleSheet, Text} from "react-native";
const Projects = ({ index, navigation }) => {
const {width} = Dimensions.get("window")
const [imageIndex, setIndex] = React.useState(0)
const ref = React.useRef()
const wItem = (width - 63) / 2;
console.log("index projects", imageIndex)
const scrollToIndex = (index)=>{
if(ref.current){
ref.current.scrollToIndex({index:index})
}
}
const goToIndex = React.useCallback((info) => {
const wait = new Promise(resolve => setTimeout(resolve, 200));
wait.then(() => {
ref.current?.scrollToIndex({ index: info.index, animated: true });
})
}, [])
return (
<ScrollView >
<TouchableOpacity onPress={()=> scrollToIndex(5)}><Text>Scroll to</Text></TouchableOpacity>
<FlatList ref={ref}
nestedScrollEnabled
numColumns={2}
data={DATA}
onScrollToIndexFailed={(index)=>goToIndex(index)}
style={styles.flatList}
keyExtractor={(i, index) => index.toString()}
renderItem={({ item, index }) => (
<View style={styles.item}>
<ImageBackground
source={item.image}
/* #ts-ignore */
imageStyle={styles.imgStyle}
style={{ width: wItem, height: wItem, alignItems: "flex-end" }}>
</ImageBackground>
</TouchableOpacity>
<Text>{item.title}</Text>
</View>
)}
/>
</ScrollView>
);
};
export default Projects;
const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
marginRight: 15,
marginBottom: 24,
alignItems:'center'
},
flatList: {
marginHorizontal: 24,
marginTop: 16,
},
imgStyle: {
borderRadius: 12,
},
});
const DATA = [//list of local images]
It is unable to call scrollTo because the ref has only been initialized and not assigned at the time of use:
const ref = useRef()
// ref = null
ref.current?.scrollToIndex({index:itemIndex,animated:true})
// ref = null
<FlatList ref={ref} .../>
// ref != null
So ref is not assigned until you render for the first time. This means that if you want to call .scrollTo() on the ref you can do it in 2 different ways:
1 - If you want to scroll to a certain index initially:
useEffect(() => {
const initialIndex = 34 //random number
if (ref.current){
ref.current.scrollToIndex({index: initialIndex})
}
}, [ref])
The ref in the dependency array will make sure that the useEffect will run whenever the value of ref changes (in this case it will change once it is assigned to a value by rendering the <Flatlist />
2 - If you want to scroll to a certain index on demand:
const handleScrollTo = (index) => {
if (ref.current && typeof ref.current.scrollToIndex === 'function'){
ref.current.scrollToIndex({index: index})
}
}
This function should be connected to something like an onPress or similar, which means the ref should be defined whenever the user presses the button because being able to see the button and press it requires that the component has rendered. Even though ref.current should already be assigned once it is used here, it is always good practice to make sure that it doesn't crash in cases where it is not, which is why we are adding the if-statement.

Need some help handling the response I'm getting from json server

I'm developing an app in React Native in which I'm trying to display some data provided by a fake API I set up using json server. I'm using the useContext hook to handle the general state of the app and since I'm fairly new to React Native and React in general I need some help handling the response I'm manipulating through the context API.
This is the State file I set up in the context folder
import React, { useReducer } from 'react'
import MenusReducer from './MenusReducer'
import MenusContext from './MenusContext'
import { baseUrl } from '../../shared/baseURL'
const MenusState = (props) => {
const initialState = {
menus: [],
selectedMenu: null
}
const [state, dispatch] = useReducer(MenusReducer, initialState)
const getMenus = async () => {
const response = await fetch(baseUrl + 'RESTAURANTES')
const data = await response.json()
console.log('This is the reducer working'); // This is a test log to see if it works
dispatch({
type: 'GET_MENUS',
payload: data
})
}
const getDetails = async (id) => {
const response = await fetch(`${baseUrl}RESTAURANTES/${id}`)
const data = await response.json()
dispatch({
type: 'GET_DETAILS',
payload: data
})
}
return (
<MenusContext.Provider value={{
menus: state.menus,
selectedMenu: state.selectedMenu,
getMenus,
getDetails
}}>
{props.children}
</MenusContext.Provider>
)
}
export default MenusState;
So here I set up a getMenus() function by which I get all the items I'd like to display in my components. As you can see, I put a test log inside the function to see if it works, which it does.
The problem comes when I try to get those items inside my app components. Here's one of the instances in which I try to get the items to display.
const Home = ({ navigation }) => {
const { menus, getMenus } = useContext(MenusContext)
const [search, setSearch] = useState('')
const [response, setResponse] = useState([])
const [categories, setCategories] = useState(allCategories)
const [loading, setLoading] = useState(true)
useEffect(() => {
const data = async () => await getMenus();
console.log('This is the app executing');
setLoading(false);
setResponse(data)
console.log(response);
}, [])
// ... some code later
return (
<ScrollView style={styles.yScroll}>
<View>
<Text style={styles.sectionTitle}>Destacados</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.sectionContainer}>
<Text>{response[0]}</Text> // Here's where I'm trying to print something about the response but it's not working
</View>
</ScrollView>
<View>
<Text style={styles.sectionTitle}>Categorias</Text>
</View>
<View style={styles.sectionContainer}>
{categories.map((item, index) => {
return (
<View key={index} style={styles.category}>
<Text>{item}</Text>
</View>
)
})}
</View>
</ScrollView>
)
}
So inside one of the ScrollViews I'm setting up a test to see if the response can be displayed, which it is not. However, inside the useEffect, I'm setting up a test log with the message 'This is the app executing' which is working, BUT, the response being logged is an empty array.
I'm sure the problem I'm facing has something to do with the asynchronous response between app and server, but I have no clear idea as to how I can address this.
Can someone please point me in the right direction? Thanks in advance!!
Based on your code, I think you can do this
const Home = ({ navigation }) => {
const { menus, getMenus } = useContext(MenusContext)
const [search, setSearch] = useState('')
const [categories, setCategories] = useState(allCategories)
const [loading, setLoading] = useState(true)
useEffect(() => {
const data = async () => await getMenus();
console.log('This is the app executing');
data();
setLoading(false);
}, [])
// ... some code later
return (
<ScrollView style={styles.yScroll}>
<View>
<Text style={styles.sectionTitle}>Destacados</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.sectionContainer}>
<Text>{menus[0]}</Text> // Here's where I'm trying to print something about the response but it's not working
</View>
</ScrollView>
<View>
<Text style={styles.sectionTitle}>Categorias</Text>
</View>
<View style={styles.sectionContainer}>
{categories.map((item, index) => {
return (
<View key={index} style={styles.category}>
<Text>{item}</Text>
</View>
)
})}
</View>
</ScrollView>
)
}

Rest API call result is not rendering in react native, how to fix that?

I have an existing React native component in which I am rendering the coins avg price fetched from other functions in the same js file.
Today, I thought to add market value to it and used an API available online.
When I ran normally it is working but in the react, it is returning NAN.
My render function.
const renderCoin = ({ item, index }) => {
console.log("---Inside render coin---" + item.text);
let { averagePrice, totalCost, totalCount } = orderListData(
item.text,
userID,
orders
);
let marketPrice = goForAxios(item.text);
console.log(`${item.text} = ${marketPrice}`);
return (
<View style={[styles1.rowFront, { flexDirection: "row" }]}>
<TouchableWithoutFeedback
onPress={() =>
props.navigation.navigate("Orders", {
coin: item.text,
userID: userID,
})
}
style={{ flex: 1 }}
>
<Text>
{item.text} {round(averagePrice)} {round(totalCost)}{" "}
{round(totalCount)} {marketPrice}
</Text>
</TouchableWithoutFeedback>
<TouchableOpacity
onPress={() => onDeleteCoin(item.id)}
style={{ flex: 1 }}
>
<Icon name={"trash"} size={30} color="#900" />
</TouchableOpacity>
</View>
);
};
And the function used to call the rest API.
const goForAxios = async (coin) => {
const response = await axios.get(
`https://min-api.cryptocompare.com/data/price?fsym=${coin}&tsyms=INR`
);
console.log(response.data.INR.toString());
return response.data.INR.toString();
};
Please suggest an alternative if my approach itself is wrong.
The function goForAxios returns a Promise. So you can't render the value until the Promise Resolved.
How about create a new State and update it after you get the API response back?
const RenderCoin = ({ item, index }) => {
console.log("---Inside render coin---" + item.text);
let { averagePrice, totalCost, totalCount } = orderListData(
item.text,
userID,
orders
);
// new state to keep the response
const [marketPrice, setMarketPrice] = useState(0);
// make api request when the component mounts
useEffect(()=> {
(async () => {
const marketPrice = await goForAxios(item.text);
setMarketPrice(marketPrice);
})();
}, []);
return (
....
)

Using memoizing selectors with Hooks in React Native

One of the components in my react native app is re-rendering several times causing problems with my derived data.
I'm using Redux to store my state and useSelector hook to retrieve the state and use it during rendering. I've read quite a bit about the use of Reselect library to avoid unnecessary rendering and optimise performance but I'm struggling to apply to my ES6 code with hooks.
This is my current code
import { useSelector, useDispatch } from "react-redux";
import...
const MovieDetailScreen = (props) => {
const selectedMovie = useSelector((state) => state.moviemain.moviemain);
const selectedMovieCast = useSelector((state) => state.moviecast.moviecast);
const selectedMovieCrew = useSelector((state) => state.moviecast.moviecrew);
return (
<View style={styles.container}>
<View>
<Text style={styles.description} numberOfLines={3}>
{selectedMovie.name}
</Text>
</View>
<View>
<Text style={styles.description} numberOfLines={3}>
{selectedMovie.overview}
</Text>
</View>
<View>
<Text style={styles.description} numberOfLines={3}>
{selectedMovie.released_date}
</Text>
</View>
<View style={styles.textLabelRow}>
{selectedMovie.genres.map((item, id) => {
return (
<Text
style={[styles.txtLabel, { backgroundColor: "#404040" }]}
key={id}
numberOfLines={1}
>
{item.name}
</Text>
);
})}
</View>
</View>
...
...
);
};
I would like to apply the Reselect to any derived data, in the example code attached it would be the mapping processing of the genres parameter of the selectedMovie state
{selectedMovie.genres.map((item, id) => {
return (
<Text
style={[styles.txtLabel, { backgroundColor: "#404040" }]}
key={id}
numberOfLines={1}
>
{item.name}
</Text>
);
})}
I have another dozens of similar scenarios where I need to filter data or work out totals and due to re-rendering I often get errors.
I believe that using Reselect, the function would only be executed if the state changes.
I tried to follow the example in here by moving my state selection outside my component and restructure my code like this
import...
import { createSelector } from "reselect";
const getMovie = createSelector(
(state) => state.moviemain.moviemain,
(moviemain) => moviemain.moviemain.map((item) => item.genres)
);
export const GenresList = () => {
const genres = useSelector(getMovie);
return (
<Text
style={[styles.txtLabel, { backgroundColor: "#404040" }]}
numberOfLines={1}
>
{genres}
</Text>
);
};
const MovieDetailScreen = (props) => {
const selectedMovie = useSelector((state) => state.moviemain.moviemain);
const selectedMovieCast = useSelector((state) => state.moviecast.moviecast);
const selectedMovieCrew = useSelector((state) => state.moviecast.moviecrew);
return (
<View style={styles.container}>
....
....
<View>
<GenresList />
</View>
</View>
...
...
);
};
but I'm getting the following error in the createSelector function
undefined is not an object (evaluating 'moviemain.moviemain.map')
I've tried other suggested solutions having all code within the main components but I get other types of errors.
I'd appreciate some guidance.
TLDR;
In the following code the first argument returns the moviemian.moviemain property, and the next line you want to get the moviemain property of that - meaning: moviemian.moviemain.moviemain which is undefined so you cant map it.
const getMovie = createSelector(
(state) => state.moviemain.moviemain,
(moviemain) => moviemain.moviemain.map((item) => item.genres)
);
Remember: what you write in a selector you get the result in the second argument.
Solution: remove the extra moviemain:
(moviemain) => moviemain.map((item) => item.genres)
Redux selectors can be tricky, here's a refresher
// you can either use multiple selectors
// declare these outside of component
const getSelectedMovie = (state) => state.moviemain.moviemain;
const getSelectedMovieCast = (state) => state.moviecast.moviecast;
const getSelectedMovieCrew = (state) => state.moviecast.moviecrew;
// or use one since its the same object
const getSelectedMovieAll = (state) => state.moviemain;
// so that you can use them inside component like
const MovieDetailScreen = (props) => {
const selectedMovie = useSelector(getSelectedMovie);
const movies = useSelector(getSelectedMovieAll);
// pay attention to the keys, they remain the same
const { moviecast: selectedMovieCast, moviecrew: selectedMovieCrew } = movies;
}
Well, that's for the redux part, about reselect, you can use it like this:
const example = createSelector(
[selector1, selector2]
(resultOfSelector1, resultOfSelector2) => ({ 'return': 'something'})
);
// so in case moviemain.moviemain is an array you can do the following
// make sure you reuse the previous selectors
const getMovie = createSelector(
[getSelectedMovie]
(moviemain) => moviemain.map((item) => item.genres)
);
export const GenresList = () => {
const genres = useSelector(getMovie);
return (

React native Unable to render the page with force update or set state

class Wait extends Component{
constructor(props) {
super(props);
this.state = { fetchingData: true, data: [], check: ''}
this.forceUpdateHandler.bind(this);
}
getData = async() => {
try {
data = await AsyncStorage.getItem('restaurants');
if (data != null) {
this.setState({fetchingData: false , data: JSON.parse(data)})
}
} catch(error){
console.log(error)
}
}
forceUpdateHandler(){
this.forceUpdate();
};
componentDidMount(){
this.getData();
}
renderRestaurant(){
return this.state.data.map((item) => {
return (
<View style ={{marginTop: 20, backgroundColor: 'red', marginTop: 20 }}>
<Text> {item.name} </Text>
<Text> {item.time} </Text>
<Text> {item.wait} </Text>
<Text> {item.people} </Text>
<Button title = 'cancel' onPress = { async () => {
let data = await AsyncStorage.getItem('restaurants');
let temp = JSON.parse(data)
let i = -1
temp.map((value, index) => {
if (value.name == item.name){
i = index;
}
})
if (i > -1){
temp.splice(i, 1)
await AsyncStorage.setItem('restaurants', JSON.stringify(temp))
}
this.forceUpdateHandler() // First way
this.forceUpdate() // Second way
this.setState({check: 'checked'}) // Third way
}
}
/>
</View>
)
})
}
render(){
const { navigate } = this.props.navigation;
const { navigation } = this.props;
return (
<View style={{width:200, height:200, justifyContent:'center', alignItems:'center', }}>
{this.state.fetchingData ? null : this.renderRestaurant()}
</View>
)
}
}
I am trying to make the page re-render each time after I click the button. Once click the button, it access the AsyncStorage and delete the corresponding element in the array, then it update the AsyncStorage with the new array and re-render the page.
I have tried the following:
1) call forUpdate directly after the update of the AsyncStorage
2) define the forceUpdateHandler function and bind it with this
3) call this.setState after the update of the AsyncStorage
But none of the above options re-renders the page. Can someone help to fix it? An example would be great! Thanks in advance.
The answer is simple. It doesn't re-render because it has nothing to re-render. It calls the render, check each component in the render if the data used to render it has changed and render them if needed. If you look at your code, you see that on the button press, you save in the async storage the new data. However, your rendering uses this.state.data to render the item. The problem is that you never update the state of your component with the new data.
Sure, you do this.setState({check: 'checked'}), but nothing in the render is using it. So there's no point in updating the UI.
An easy way to fix it would be to call this.getData() at the end of the button onPress. That way, you would update the data state which would update your UI.
Get the updated list of restaurants { removing the selected restaurant}
Stored the updated list to Asyncstorage.
fetch the updated list from asyncStorage and set the state.
storeData = async (restaurants) => {
try {
await AsyncStorage.setItem('restaurants', JSON.stringify(restaurants))
} catch (error) {
console.log("Error", error);
}
}
renderRestaurant(){
return this.state.data.map((item, index, restaurants) => {
return (
<View key={index}>
<Text> {item.name} </Text>
<Text> {item.time} </Text>
<Button
title = 'cancel'
onPress = {() => {
let restaurantListWithoutCurrentRestaurant = restaurants.filter((restaurant)=> restaurant.name !== item.name);
this.storeData(restaurantListWithoutCurrentRestaurant);
this.getData();
}}/>
</View>
)
})
}
I think this will solve your problem.