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 (
Related
I have three buttons which should pass a string value to a function that sorts the movies I have in my app based on the word (it starts a query).
const HomeScreen = () => {
const navigation = useNavigation();
const [movies, setMovies] = useState({});
useEffect(() => {
getMovies();
},[])
useEffect(() => {
getMoviesFiltered();
},[])
const [filter, setFilter] = useState('');
const getMoviesFiltered = async (filter) =>{
const querySnapshot = await getDocs(query(collection(db, "movies"), orderBy(filter)));
setMovies(querySnapshot.docs);
}
const getMovies = async () =>{
const querySnapshot = await getDocs(query(collection(db, "movies"), orderBy('name')));
setMovies(querySnapshot.docs);
}
return (
<View>
<View>
<TouchableOpacity onPress = {() => {setFilter('name')}}>
<Text>Title</Text>
</TouchableOpacity>
<TouchableOpacity onPress = {() => {setFilter('runtime')}}>
<Text>Runtime</Text>
</TouchableOpacity>
<TouchableOpacity onPress = {() => {setFilter('year')}}>
<Text>Year</Text>
</TouchableOpacity>
</View>
</View>
<FlatList
data = {movies}
renderItem = {({item}) => (
<TouchableOpacity onPress={() => navigation.navigate('Modal', item.data())}>
<Image source={{uri: item.data().pic}}/>
</TouchableOpacity>
)
}
/>
</View>
)
}
export default HomeScreen
I can see that it updates the filter value when I press the button twice (I've read that setState is an asynchronous operation, so I get why that happens), but I'm not sure how to properly pass that string into the getMovies1 function.
Could someone please help me out?
It looks like you are trying to pass the filter state value to the getMoviesFiltered function when it is called. However, you are not passing any arguments to the getMoviesFiltered function when you call it in the TouchableOpacity onPress event handlers.
To fix this, you can pass the filter state value as an argument to the getMoviesFiltered function in the onPress event handlers like this:
<TouchableOpacity onPress = {() => {setFilter('name');
getMoviesFiltered(filter)}}>
<Text>Title</Text>
</TouchableOpacity>
<TouchableOpacity onPress = {() => {setFilter('runtime');
getMoviesFiltered(filter)}}>
<Text>Runtime</Text>
</TouchableOpacity>
<TouchableOpacity onPress = {() => {setFilter('year');
getMoviesFiltered(filter)}}>
<Text>Year</Text>
</TouchableOpacity>
You may also want to consider moving the useEffect hook that calls getMoviesFiltered to depend on the filter state value. This will ensure that getMoviesFiltered is only called when the filter state value changes. You can do this like this:
useEffect(() => {
getMoviesFiltered(filter);
},[filter])
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>
)
}
i'm having an messages screen and i need to navigate to a "single message" when tapping to the List item of messages but i get this error "you need to specify name or key when calling navigate with an object as the argument"
i have created the "single message" screen and added it as a <Stack.Screen/> also but i don't know what i'm doing wrong.
below is my code:
function MessagesScreen({navigation}) {
const [messages, setMessages] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const loadMessages = async () => {
const response = await messagesApi.getMessages();
setMessages(response.data);
}
useEffect(() => {
loadMessages();
}, []);
const handleDelete = message => {
setMessages(messages.filter((m) => m.id !== message.id));
}
return (
<Screen>
<FlatList
data={messages}
keyExtractor={message => message.id.toString()}
renderItem={({ item }) =>
<ListItem
title={item.fromUserId}
subTitle={item.content}
image={item.image}
onPress={() => navigation.navigate(routes.MESSAGE_SINGLE, item)}
renderRightActions={() =>
<ListItemDeleteAction onPress={() => handleDelete(item)} />}
/>
}
ItemSeparatorComponent={ListItemSeparator}
refreshing={refreshing}
onRefresh={() => {
setMessages([
{
id: 1,
title: 'T1',
description: 'D1',
image: require('../assets/mosh.jpg')
},
])
//setMessages(loadMessages());
}}
/>
</Screen>
);
}
const styles = StyleSheet.create({
})
export default MessagesScreen;
when i'm logging the "onPress" event on the console like this:
onPress={() => console.log('message selected', item)}
heres what i get:
and below is the MessageSingle screen i created to render the message but i dont know how to do it.
function MessageSingle() {
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}
const styles = StyleSheet.create({
container: {}
});
export default MessageSingle;
so i want to get the message from the list of the messages. maybe i dont have to create e separate screen? i'm a beginner on this
any help would be appreciated!
you need to first add your MessageSingle component to the navigation container. Just put it as one of the screens along your MessagesScreencomponent. Then you need to navigate to it using that name:
onPress={() => navigation.navigate('MessageSingle', {item})}
the above will navigate to the screen with name MessageSingle, and passing the object item as a param.
in order to access this in your MessageSingle component, you need to use the route props.
function MessageSingle({route}) {
console.log('item = ', route.params?.item); // this would be your item.
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}
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
}
I'm new in react native and I'm tryng to refactor this code below, and I think I'm doing some thing wrong here "setSentences(item)", because it's not updating.
Do you know what i'm doing wrong here?
this.state = {
sentences: [],
};
{this.state.sentences.map((item) => {
return(
<TouchableOpacity
onPress={() => {
item.selected = false;
this.setState(item);
}}>
</TouchableOpacity>
)}}
Refactored:
const [sentences, setSentences] = useState([]);
{sentences.map((item) => {
return (
<TouchableOpacity
onPress={() => {
item.selected = false;
setSentences(item);
}}>
</TouchableOpacity>
sentences is an array but when you show list array setSentences update array sentences. Something wrong here
const [sentences, setSentences] = useState([]);
{sentences.map((item) => {
return (
<TouchableOpacity
onPress={() => {
item.selected = false;
setSentences(item);
}}>
</TouchableOpacity>
I think you can use useRef to store array sentences and another state to set item
const sentences = useRef([]);
const [itemSentence, setItemSentence] = useState('');
<Text>{itemSentence}</Text>
{sentences.current.map((item) => {
return (
<TouchableOpacity
onPress={() => {
item.selected = false;
setItemSentence(item);
}}>
</TouchableOpacity>
For clear solution you have to create one method like this :
const onItemPress = (itemIndex) => {
const temp = [].concat(sentences);
temp[itemIndex].selected = false;
setSentences(temp);
}
Now, call this method on TouchableOpacity onPress method and pass current index to this method to modify that perticular item state as below :
{sentences.map((item, itemIndex) => {
return (
<TouchableOpacity
onPress={() => onItemPress(itemIndex)}>
</TouchableOpacity>
)
}}
And note, your state should be :
const [sentences, setSentences] = useState([]);
i think you need clone array sentences and get index inside
const [sentences, setSentences] = useState([]);
sentences.map((item:any,index:number) => {
return (
<TouchableOpacity
onPress={() => {
const cloneSentences =clone(sentences);
cloneSentences[index].selected = false;
setSentences(cloneSentences);
}}>
</TouchableOpacity>)
})