First, I run a graphql query and then use its results to render some items using the FlatList. This works fine.
On another screen, I run another mutation using apollos refetch option, such that whenever that mutation is successful, the first query will refetch data where ever it is used. The refetching part works fine and I have tested it on other screens. So automatically, the data in WhiteList is updated.
However the friendList is not updated so the FlatList doesn't get updated.
How can I fix this? useEffectcould have been an option but using the custom graphql hook inside it gives me invalid hook call error. What else can I do?
export const WhitelistBar: React.FC = () => {
const [friendList, setFriendList] = useState<Friend[]>();
const [originatorId, setOriginatorId] = useState<number>();
useEffect(() => {
//console.log('friendlist', friendList);
}, [useGetMyProfileQuery]);
const _onCompleted = (data) => {
console.log('running', data);
let DATA = data.me.friends.nodes.map(
(item: {
id: number;
firstName: string;
rating: number;
vehicles: Vehicle[];
friends: UserConnection;
}) => ({
id: item.id,
imageUrl: defaultUrl,
name: item.firstName,
rating: item.rating,
vehicles: item.vehicles,
numberOfFriends: item.friends.totalCount,
}),
);
setFriendList(DATA);
console.log('daattaaa', DATA);
setOriginatorId(data.me.id)
};
const _onError = () => {
let DATA = [
{
id: 1,
imageUrl: defaultUrl,
name: 'Friend',
},
{
id: 2,
imageUrl: defaultUrl,
name: 'Friend',
},
{
id: 3,
imageUrl: defaultUrl,
name: 'Friend',
},
];
setFriendList(DATA);
setOriginatorId(0);
};
const { data } = useGetMyProfileQuery({
onCompleted: _onCompleted,
onError: _onError,
});
return (
<View style={scaledStyles.container}>
<View style={scaledStyles.whiteListBarTitle}>
<Text style={scaledStyles.whiteListBarText}>Meine Freunde</Text>
</View>
<View style={{ flexDirection: 'row' }}>
<FlatList
showsHorizontalScrollIndicator={false}
data={friendList}
horizontal={true}
renderItem={({ item }) => (
<WhitelistItem
title={item.name}
face={item.imageUrl}
numberOfFriends={item.numberOfFriends}
vehicles={item.vehicles}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
</View>
</View>
);
};
Edit:
I also tried this but this renders nothing for now:
const showFlatList = (friendList: Friend[] | undefined) => {
return (
<FlatList
showsHorizontalScrollIndicator={false}
data={friendList}
horizontal={true}
scrollEnabled
renderItem={({ item }) => (
<WhitelistItem
title={item.name}
face={item.imageUrl}
firstName={item.name}
rating={item.rating}
numberOfFriends={item.numberOfFriends}
vehicles={item.vehicles}
originatorId={originatorId}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
);
};
useEffect(() => {
showFlatList(friendList);
}, [useGetMyProfileQuery]);
and this in the main return instead of the FlatList:
{data && showFlatList}
Also, tried this but Argument of type '(DATA: any) => void' is not assignable to parameter of type 'EffectCallback'.ts(2345)
useEffect((DATA: any) => {
setFriendList(DATA)
}, [DATA])
You can't use hook as effect dependency - only variables/props/values.
You can use hook inside effect function (body).
If it is a component dependent on other (parent's) data/values - pass them as prop and use in effect dependency.
Related
Trying to manage state between parent/child components in React Native. Using functional components and hooks. I want my Collection component (the parent) to be able to update state of an array after calling an onDelete function from the child. I keep tweaking the function but can't seem to get it to work. Thanks in advance!
Parent:
const Collection = () => {
const navigation = useNavigation();
const [showAddGuitar, setShowAddGuitar] = useState(false);
const [gtrlist, setGtrlist] = useState(GUITARS);
const [selectedGuitar, setSelectedGuitar] = useState({});
// Open Details Page
const openDetails = (guitar) => {
setSelectedGuitar(guitar);
navigation.navigate("Details", {
id: guitar.id,
year: guitar.year,
brand: guitar.brand,
model: guitar.model,
sn: guitar.sn,
description: guitar.description,
history: guitar.history,
});
};
// Delete Guitar
const deleteGuitar = (id) => {
setGtrlist(gtrlist.filter((guitar) => guitar.id !== id));
console.log(gtrlist);
};
return (
<View>
{showAddGuitar && (
<GuitarModal onAdd={addGuitar} onCloseModal={onCloseModal} />
)}
{gtrlist.length <= 0 && <Text>Collection empty</Text>}
{gtrlist.length > 0 && (
<FlatList
onDelete={deleteGuitar}
data={gtrlist}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<Text
style={styles.item}
onPress={() => openDetails(item)}
>
{item.year} {item.brand} {item.model}
</Text>
)}
/>
)}
<Button
title="Add New Guitar"
style={[styles.footer, styles.button, styles.buttonOpen]}
onPress={handleAddNewGuitarPress}
/>
</View>
Child:
const DetailsPage = ({ route, navigation, onDelete }) => {
const { id, year, brand, model, sn, description, history } = route.params;
return (
<View>
<Text>
{year} {brand} {model}
</Text>
<Text>S/N: {sn}</Text>
<Text>Description: {description}</Text>
{history &&
history.map((item) => (
<Text key={item.id}>
{item.date} {item.item} {item.cost}
</Text>
))}
<View>
<Button title="Go back" onPress={() => navigation.goBack()} />
<Button
title="Delete guitar"
onPress={() => onDelete(id)}
/>
</View>
</View>
);
};
You can't destructure onDelete from props since you don't pass the onDelete with props. Actually you don't pass it at all. While you passed other details with params, you can also pass onDelete functions with that as well.
const openDetails = (guitar) => {
setSelectedGuitar(guitar);
navigation.navigate("Details", {
id: guitar.id,
year: guitar.year,
brand: guitar.brand,
model: guitar.model,
sn: guitar.sn,
description: guitar.description,
history: guitar.history,
deleteGuitar,
});
};
then of course you need to destructure it on the DetailsPage. But I'm not quite sure what happens after you delete the guitar since you won't have details on that page.
*I use the ref property in the flatlist component in react native, but I get an undefined is not an
object error. I looked at many places on
the internet but did not get a satisfactory answer. Please help me :)*
const DATA = [
{
id: 0,
title: "First Item",
},
{
id: 1,
title: "Second Item",
},
];
const [selectedId, setSelectedId] = useState(null);
**React.useEffect(() => {
this.el.scrollToIndex({ animated: true, index:selectedId});
}, []);**
const Item = ({ item, onPress, style }) => (
<TouchableOpacity onPress={onPress} style={[styles.item, style]}>
<Text style={styles.title}>{item.title}</Text>
</TouchableOpacity>
);
const renderItem = ({ item }) => {
const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
style={{ backgroundColor }}
/>
);
};
return (
<>
<FlatList
data={DATA}
horizontal={true}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
**ref={(el) => this.el = el}**strong text
/>
</>
)
}```
To use a ref, you need to use useRef (since you're on a function component). On function components you won't find yourself using this. When using useRef, you'll need to use .current to access the item itself. So, you can see I'm doing el.current.scrollToIndex
Also, you'll need to add selectedId as a dependency to your useEffect
export default function App() {
const DATA = [
{
id: 0,
title: "First Item",
},
{
id: 1,
title: "Second Item",
},
{
id: 2,
title: "Second Item",
},
{
id: 3,
title: "Second Item",
},
{
id: 4,
title: "Second Item",
},
{
id: 5,
title: "Second Item",
},
{
id: 6,
title: "Second Item",
},
];
const [selectedId, setSelectedId] = React.useState(null);
const el = React.useRef()
React.useEffect(() => {
el.current.scrollToIndex({ animated: true, index:selectedId});
}, [selectedId]);
const Item = ({ item, onPress, style }) => (
<TouchableOpacity onPress={onPress} style={[styles.item, style]}>
<Text style={styles.title}>{item.title}</Text>
</TouchableOpacity>
);
const renderItem = ({ item }) => {
const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
style={{ backgroundColor }}
/>
);
};
return (
<>
<FlatList
data={DATA}
horizontal={true}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
ref={el}
/>
</>
)
}
I'm working on a system to multi-select elements from a FlatList to then remove from AsyncStorage the data related to the selected elements. How it works is:
-Parent component passes to child inside FlatList a state that will hold children's functions, like this:
export default function Parent({ navigation }) {
const [ productsInfo, setProductsInfo ] = useState([]);
const [ selectedProducts, setSelectedProducts ] = useState([]);
const [ selectCount, setSelectCount ] = useState(0);
<FlatList data={productsInfo}
renderItem={({ item }) => (
<Child item={item}
selectedProducts={selectedProducts}
selectCount={selectCount}
setSelectCount={setSelectCount}/>
)}/>
-Children components fill parent's array state with their callback functions:
const Child = ({ item, selectedProducts, selectCount, setSelectCount }) => {
const [ selected, setSelected ] = useState(false);
useEffect(() => {
selectedProducts.push([selected, setSelected]); //filling parent's state
}, []);
-Parent handles children's state through callback functions passed on previous step (with useEffect)
export default function Parent({ navigation }) {
const [ selectedProducts, setSelectedProducts ] = useState([]);
const [ selectCount, setSelectCount ] = useState(0);
const selectAll = () => {
selectedProducts.map(product => {
let setSelected = product[1]; //second element is the setState of child
setSelected(true);
});
setSelectCount(selectedProducts.length);
}
-Children handle their own state when they're pressed:
const Child = ({ item, selectedProducts, selectCount, setSelectCount }) => {
const [ selected, setSelected ] = useState(false);
const handleSelect = () => {
setSelected(!selected);
let count = selected ? selectCount - 1 : selectCount + 1;
setSelectCount(count); //parent's state
}
Now this all works, everything gets highlighted correctly and the counter of selected elements checks out. The problem is when I go to confirm deletion of selected elements (or the data they're rendered upon) and I iterate through the children's boolean state. They all turn out as false, even if they're true since they are highlighted, since they change color based on that state.
export default function Parent({ navigation }) {
const deleteSelected = async () => {
await selectedProducts.map(product => {
let selected = product[0]; //first element is the state's value, boolean
console.log(selected); //all false
});
}
It needs to know at which index there are true value so it can delete data at those indexes, but they never turn out true.
Do you know why this happens? I'll leave the whole code below.
import React, { useEffect, useState } from 'react';
import {
FlatList,
StyleSheet,
View,
Text,
TouchableHighlight,
Alert
} from 'react-native';
import Icon from '#expo/vector-icons/MaterialIcons';
import AsyncStorage from '#react-native-async-storage/async-storage';
import { TouchableNativeFeedback } from 'react-native-gesture-handler';
const Product = ({ item, selectedProducts, selectCount, setSelectCount }) => { //Child
const [ selected, setSelected ] = useState(false);
const handleSelect = () => {
setSelected(!selected);
let count = selected ? selectCount - 1 : selectCount + 1;
setSelectCount(count);
}
useEffect(() => {
selectedProducts.push([selected, setSelected]);
}, []);
return (
<TouchableHighlight onPress={() => handleSelect()}>
<View style={[styles.productContainer, {backgroundColor: selected ? '#DDDDDD' : '#F8F8F8'}]}>
<View>
<Text style={styles.productTitle}>{item.name}</Text>
</View>
<View style={styles.productDetailsContainer}>
<View style={styles.productDetails}>
<Text style={styles.productDetailsTitle}>Prezzo attuale</Text>
<Text>{item.currentPrice}</Text>
</View>
<View style={styles.productDetails}>
<Text style={styles.productDetailsTitle}>Prezzo piĆ¹ basso</Text>
<Text>{item.lowestPrice}</Text>
</View>
<View style={styles.productDetails}>
<Text style={styles.productDetailsTitle}>Prezzo target</Text>
<Text>{item.targetPrice}</Text>
</View>
</View>
</View>
</TouchableHighlight>);
}
export default function WatchlistDeleteScreen({ navigation }) { //Parent
const [ productsInfo, setProductsInfo ] = useState([]);
const [ selectedProducts, setSelectedProducts ] = useState([]);
const [ selectCount, setSelectCount ] = useState(0);
const selectAll = () => {
selectedProducts.map(product => {
let setSelected = product[1];
setSelected(true);
});
setSelectCount(selectedProducts.length);
}
const deleteSelected = async () => {
await selectedProducts.map(product => {
let selected = product[0];
console.log(selected);
});
}
useEffect(() => {
const getData = async () => {
await AsyncStorage.getItem('productsInfo')
.then(data => JSON.parse(data))
.then(jsonData => {
setProductsInfo(jsonData);
})
.catch(error => {});
}
getData();
}, []);
return (
<View style={{flex: 1}}>
<View style={styles.header}>
<View style={styles.headerIconsContainer}>
<TouchableNativeFeedback onPress={() => { navigation.pop() }}
background={TouchableNativeFeedback.Ripple('default', true)}>
<Icon name={'close'} size={27} color={'black'} />
</TouchableNativeFeedback>
<Text style={{fontWeight: 'bold', fontSize: 20, marginLeft: 25}}>{selectCount}</Text>
</View>
<View style={styles.headerIconsContainer}>
<View style={styles.icons}>
<TouchableNativeFeedback onPress={() => { selectAll() }}
background={TouchableNativeFeedback.Ripple('default', true)}>
<Icon name={'select-all'} size={27} color={'black'} />
</TouchableNativeFeedback>
</View>
{selectCount > 0 &&
<View style={styles.icons}>
<TouchableNativeFeedback onPress={() => { deleteSelected() }}
background={TouchableNativeFeedback.Ripple('default', true)}>
<Icon name={'check'} size={27} color={'black'} />
</TouchableNativeFeedback>
</View>}
</View>
</View>
<FlatList data={productsInfo}
renderItem={({ item }) => (
<Product item={item}
selectedProducts={selectedProducts}
selectCount={selectCount}
setSelectCount={setSelectCount}/>
)}/>
</View>
);
}
const styles = StyleSheet.create({
header: {
backgroundColor: 'white',
height: 58,
elevation: 4,
justifyContent: 'space-between',
paddingLeft: 10,
paddingRight: 10,
flexDirection: 'row',
alignItems: 'center'
},
headerIconsContainer: {
flexDirection: 'row',
alignItems: 'center'
},
screenTitle: {
fontWeight: 'bold',
fontSize: 20
},
productContainer: {
padding: 10
},
productTitle: {
fontWeight: 'bold',
fontSize: 19,
borderBottomColor: 'grey',
borderBottomWidth: 0.2
},
productDetailsTitle: {
fontWeight: 'bold',
fontSize: 14
},
productDetailsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 5,
justifyContent: 'space-between'
},
productDetails: {
flexWrap: 'wrap'
},
icons: {
marginLeft: 25
}
});
The problem
This is wrong:
useEffect(() => {
selectedProducts.push([selected, setSelected]);
}, []);
The selectedProducts is a state variable and should be considered immutable: push does not mutates the variable itself (its the same array), but adds an element to it (a side effect). You should avoid this and only use setSelectedProducts instead. Even more, in the browser console you should have a message warning about useEffect not having the correct dependencies array. Try this instead:
// Add extra argument setSelectedProducts
const Child = ({ item, selectedProducts, setSelectedProducts, selectCount, setSelectCount }) => {
const [ selected, setSelected ] = useState(false);
useEffect(() => {
// Filling parent's state
setSelectedProducts([
...selectedProducts,
[selected, setSelected],
]);
}, [setSelectedProducts, selectedProducts, selected, setSelected]);
Now, this shows part of the problem here: there are circular dependencies. In this fixed useEffect example, it will be fired each time one or more of its dependencies changes. As we are changing selectedProducts in the callback (indirectly, using setSelectedProducts), it will be called again, and again, and again.
The solution
You need to pass the responsibility of managing the list of states (and, thus, the list of selected ones) to the list containing them (here, WatchlistDeleteScreen). This will receive a (un)select me message from each product:
const Product = ({ item, selected, toggleSelected }) => { //Child
return (
<TouchableHighlight onPress={toggleSelected}> // Without `()`: No extra function
//...
</TouchableHighlight>
);
}
export default function WatchlistDeleteScreen({ navigation }) { //Parent
const [ productsInfo, setProductsInfo ] = useState([]);
const [ selectedProducts, setSelectedProducts ] = useState([]);
// Mark all products as selected
const selectAll = () => {
setSelectedProducts(
productsInfo.map(() => true)
);
}
// Mark all products as unselected
const deleteSelected = () => {
setSelectedProducts(
productsInfo.map(() => false)
);
}
// Toggles a single product selection state
const toggleSelected = (index) => {
const selectedProductsNew =
selectedProducts.map(
// Only toggle the matching index product
(poductState, i) => i === index ? !value : value
);
// But full array is updated, respecting React
// immutability and state change controller
setSelectedProducts( selectedProductsNew );
}
// Fetch items. No need to async/await if you use .then/.catch
useEffect(() => {
AsyncStorage.getItem('productsInfo')
.then(data => JSON.parse(data))
.then(jsonData => {
setProductsInfo(jsonData);
// Also create an array with the states of all
// products to unselected
deleteSelected();
})
.catch(error => {});
}, [setProductsInfo, deleteSelected]);
// Just to be clear. You can use selectedProducts.length directly
// from the JSX
const selectCount = selectedProducts.length;
return (
// ...
<FlatList
data={productsInfo}
// Also get `index` from FlatList
// https://reactnative.dev/docs/flatlist#renderitem
renderItem={({ item, index }) => (
<Product
item={item}
// Pass `selected` state (managed in parent, used in child)
selected={selectedProducts[index]}
// Pass `index` to the `toggleSelected` function
toggleSelected={toggleSelected.bind(this, index)}/>
)}
/>
// ...
)
}
I have a list of category from calling external API and I am displaying it in a flatlist like this in horizontal scroll
but it is not changing state onPress , I want it should change color when user click on particular tab/button
my API json format data is like this
const data = [
{
id: '1',
title: 'General'
},
{
id: '2',
title: 'Student-Visa'
},
{
id: '3',
title: 'Study'
},
{
id: '4',
title: 'Festival'
},
{
id: '5',
title: 'NorthIndian-Food'
},
]
and I am using it as
const renderItem = ({ item, index }) => (
<View key={index} style={selectedCategory === item.title ? styles.activeCategory : styles.categoryContainer}>
<TouchableOpacity
activeOpacity={0.6}
underlayColor={COLORS.white}
onPress={()=>handleCategory(item.title)}
>
<Text>{item.title}</Text>
</TouchableOpacity>
</View>
);
return (
<List
data={data}
renderItem={renderItem}
horizontal={true}
style={styles.container}
contentContainerStyle={styles.contentContainer}
showsHorizontalScrollIndicator={false}
keyExtractor={item => item.id.toString()}
/>
)
const handleSelectedCategory = (title) => {
setSelectedCategory(title);
console.log(selectedCategory);
}
const [selectedCategory, setSelectedCategory] = useState();
I am using it as a separate component in another component
<FAQcategory selectedCategory={selectedCategory} />
any suggestion ? or help what I am doing wrong
Thanks in advance
This might help
const [selectedCategory, setSelectedCategory] = useState(0); // should be item index
const handleCategory = (index) => {
setSelectedCategory(index);
};
const renderItem = ({ item, index }) => (
<View
key={index}
style={
selectedCategory === index
? styles.activeCategory
: styles.categoryContainer
}
>
<TouchableOpacity
....
onPress={() => handleCategory(index)} // send index in param
>
....
</TouchableOpacity>
</View>
);
I followed this tutorial in order to have a slider with images : https://www.youtube.com/watch?v=HAgJqaJc-ck
I have three main files :
Carousel.js
const Carousel = ({data}) => {
const scrollX = new Animated.Value(0)
let position = Animated.divide(scrollX, width)
if (data && data.length){
return (
<View>
<FlatList data = {data}
keyExtractor= {(item, index) => 'key' + index}
horizontal
pagingEnabled
scrollEnabled
snapToAlignment = 'center'
scrollEventThrottle = {16}
decelerationRate = {"fast"}
showsHorizontalScrollIndicator = {false}
renderItem = {({item}) => {
return <CarouselItem item= {item} navigation={props.navigation}/>
}}
onScroll = {Animated.event(
[{nativeEvent : {contentOffset: { x : scrollX}}}]
)}
/>
<View style = {styles.dotView}>
{data.map((_, i) => {
let opacity = position.interpolate({
inputRange : [i - 1, i, i + 1],
outputRange: [0.3, 1, 0.3],
extrapolate : 'clamp'
})
return <Animated.View
key={i}
style = {{opacity, height : 10, width : 10, background: '#595959', margin: 8 , borderRadius: 5}}
/>
})}
</View>
</View>
)
}
console.log('Please provide Images')
return null
}
CarouselItem.js
const CarouselItem = ({item, navigation}) => {
const url = item.url
return (
<View style={styles.cardView} >
<TouchableOpacity onPress={() => props.navigation.navigate("BoatInfo")}><Image style={styles.image} source={{uri: item.url}} /></TouchableOpacity>
<View style={styles.textView}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.imageDescription}>{item.description}</Text>
</View>
</View>
)
}
And Data.js
export const dummyData = [
{
title: 'BOAT DETAILS', url : require('../component/schema.jpg'),
description: 'PDF containing the specificities of the boat', id : 1
},
{
title: 'TUTORIALS', url : require('../component/tuto.png'),
description: 'Become a capitain by watching these videos', id : 2
},
{
title: 'YOUR TRIP', url : require('../component/trip.png'),
description: 'Follow your trip details', id : 3
},
]
Now, As you see in CarouselItem, I am trying to add a functionality so that when I press on the image, it would take me to another page (each picture takes me to a different page). However, when I try to do so, I get the following error:
So, I understand that in props it is empty...
But when I switch const CarouselItem = ({ item }, props) => { to const CarouselItem = ( props, { item }) => { Then I get the following error:
What I don't understand is that when I remove {item} then the navigation works and when I remove props then {item} works... How can both work? What am I doing wrong ?
Only screens in React Native automatically have access to the navigation prop, so you'll need to pass it to the CarouselItem component, then use the always fun object destructuring to get your particular props in the component (like you did with item). It should look like this:
...
renderItem = {({item}) => {
return <CarouselItem item={item} navigation={navigation}/>
}}
...
and then:
const CarouselItem = ({ item, navigation }) => {
...
and that should work!
Good luck!
You have to pass navigation prop object from flatlist with item object too something like this
navigate ={props.navigation} item={item}
in crousal function you have to write like this
crousalItem =(item, navigation) =>
This way you got both item and navigation object.
use item to list data, navigation.navigate() to move to another screen