I know that extraData is used to update flatlist when it changes but somehow it does not work in my case. I know I am doing something wrong but I need advice to figure that problem out.
Here is my flatList:
<FlatList
data={this.state.data}
extraData={this.state.data}
renderItem={this.renderPost}
keyExtractor={(item, index) => index.toString()}
onEndReached={this.loadMorePosts}
onEndReachedThreshold={0.5}
ListFooterComponent={this.renderFooter}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={this.state.loading}
onRefresh={this.loadNewerPosts}
/>
}
/>
and here is my deleteRequest that should remove one item from this.state.data:
deletePost = (index) => {
console.log(this.state.data.length);
let data = this.state.data;
data.splice(index, 1);
this.setState({ data: data }, () => {
console.log(this.state.data.length);
});
};
I even tried to put refresh in state and to change it every time I delete item and put it as extraData but nothing happens. What am I doing wrong?
This.state.data.length is changing so the data changes but flatList do not re-renders.
Do something like
deletePost = (index) => {
....
let data = [...this.state.data]; // here is a logic
data.splice(index, 1);
this.setState({ data: data }, () => {
console.log(this.state.data.length);
});
}
If you want to use the Pull To Refresh feature try something like this:
refreshFlatlist = () => {
this.setState(
{
refresh: true,
},
() => this.getDataHandler() // whatever updates your dataArray displayed in the list
);
this.setState({
refresh: false,
});
};
Now the flatlist RefreshComponent looks like this:
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refresh}
onRefresh={this.refreshFlatlist}
/>
}
extraData={this.state.refresh}
data={this.state.data}
keyExtractor={(item, index) => item.id.toString()}
renderItem={({ item }) => ( ...
Don't use splice. Try below snippet it's working as expected.
deletePost = index => {
const newData = this.state.data.filter((item, i) => i !== index);
this.setState({
data: newData,
});
};
render(){
return (
<FlatList
data={this.state.data}
extraData={this.state.data}
renderItem={this.renderItem}
/>
);
}
deletePost = (index) => {
let data = this.state.data;
data.splice(index, 1);
this.setState({ data: [] }, () => {
this.setState({ data: data });
});
};
Thanks all. With your solutions I came up with this idea and it works perfectly.
Related
I'm developing an app which has to fetch new orders from the Firestore database, I used componentDidMount to refresh the screen every 10 seconds and launch the fetchNewOrders function, if new orders are available, the function should push that object into the state array newOrder, and display the orders in the FlatList below. When I start the code it returns the error TypeError: undefined is not an object (evaluating 'item.id'), I also wrote the example of an array I'm fetching from the database.
Screen
export default class Received extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
newOrder: [],
};
}
async componentDidMount() {
this.updateTimer = setInterval(() => {
this.fetchNewOrders();
}, 10000);
}
fetchNewOrders = async () => {
const querySnapshot = await getDocs(collection(db, path));
if(querySnapshot.length !== 0) {
querySnapshot.forEach((doc) => {
let array = this.state.newOrder;
const data = doc.data().order;
data.map(({obj, id}) => {
const filter = array.find(c => c.id === id);
if (filter == undefined) {
array.push(obj)
this.setState({ newOrder: array })
}
})
})
}
}
render() {
return (
<View>
<FlatList
data={this.state.newOrder}
keyExtractor={item => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity>
<Text>{item.serviceRequested}</Text>
<View>
<Tex>${item.total}</Text>
</View>
</TouchableOpacity>
)
}}
/>
</View>
)
}
}
data (new order)
Array [
Object {
"date": "Mon Feb 28 2022 11:24:14 GMT-0500 (EST)",
"id": 0.9436716663143794,
"instructions": "",
"order": Array [
/////////////
],
"paymentType": "Cash",
"serviceRequested": "Delivery",
"total": 10.4,
},
]
setState is an async function that schedules a render with new state. It should only be called once per render, not in a loop.
const append = [];
querySnapshot.forEach((doc) => {
const data = doc.data().order;
// Is there bad data without ids?
data.filter(c => c.id && !this.state.newOrder.some(no => no.id === c.id))
.forEach((d) => append.push(d));
});
// Create the new state, then set it once.
this.setState({ newOrder: [...this.state.newOrder, ...append]});
I would suggest filtering your data (this.state.newOrder) first. This would make it so that you only display items that have ids.
Suggested Change:
<FlatList
data={this.state.newOrder.filter((order)=>order.id)}
keyExtractor={item => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity>
<Text>{item.serviceRequested}</Text>
<View>
<Tex>${item.total}</Text>
</View>
</TouchableOpacity>
)
}}
/>
Above is code to fix this issue as described, but I would suggest that you make sure the Firestore only sends data that has id's if possible. Obviously, this may be out of your hands, but I would make sure that the Firestore is giving you reliable data, as it could cause more problems down the road.
Agenda doesn't update even when new data are added. Only when the app is reloaded it updates.
Here is my code:
const CalendarScreen = () => {
const list = useSelector((state) => state.getTodo.list);
const [items, setItems] = useState({});
const loadItems = () => {
list.forEach((data) => {
const strTime = data.createdDate;
if (!items[strTime]) {
items[strTime] = [];
list.forEach((datalist) => {
items[strTime].push({
name: datalist.title,
});
});
}
});
const newItems = {};
Object.keys(items).forEach((key) => {
newItems[key] = items[key];
});
setItems(newItems);
};
const renderItem = (item) => {
return (
<View >
<Text>{item.name}</Text>
</View>
);
};
return (
<View style={flex: 1}>
<Agenda
items={items}
loadItemsForMonth={loadItems}
renderItem={renderItem}
pastScrollRange={1}
futureScrollRange={1}
/>
</View>
);
};
export { CalendarScreen };
Expectation: Just want the Agenda to update automatically when new data is added in the state instead of having to reload the app.
It looks like that refresh depends call of loadItemsForMonth.
Unfortunately I cannot see when Agenda call loadItemsForMonth
I'm calling an Api to get data but the data is really heavy. I'm wondering if i'm calling it in right place inside useEffect or should i call it somewhere else. I've put the console.log to check but the number of console.log exceeded the number of objects i have in the API. My code is :
const ProductsList = () => {
const [products, setProducts] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
getProducts().then((response) => {
if (isMounted) {
console.log('im being called');
setProducts(response);
setLoading(false);
}
});
return () => { isMounted = false; };
}, [products]);
return (
<View style={styles.container}>
{isLoading ? <ActivityIndicator /> : ((products !== [])
&& (
<FlatList
data={products}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<Item
style={{ marginLeft: 35 }}
name={item.name}
date={item.date}
address={item.adress}
/>
);
}}
/>
)
)}
</View>
);
};
It looks like your effect goes round in a circle:
On each render the effect will look at products to see if it has changed.
If it has changed it will call your effect which fetches new products.
When you get new products you update your products state.
This causes the effect to run again.
You probably only want to run that effect once, when the component mounts. In which case you can simply write:
useEffect(() => {
getProducts().then((response) => {
setProducts(response);
setLoading(false);
});
}, []); // <-- note the empty array, which means it only runs once when mounted
If that single API call is too heavy, then you need to look more at things like pagination in your requests, or modifying the response to return only the data you really need. But that's outside the scope of this question, I think.
Let me know if you have any questions.
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() { ... }
}
Here is my posts page code, it fetches post titles from my API on load and this works perfect. The problem is that once it's loaded if a new post is added to API and I pull up to refresh it doesn't get new posts even though my onRefresh function works because I can trigger an alert in it.
The only way I can get new posts in API to show after they were loaded is by reloading the application itself.
componentDidMount() {
this.fetchData()
}
constructor(props) {
super(props);
this.state = {
refreshing: true,
data: []
};
}
fetchData = () => {
const url = 'myAPIurl';
fetch(url)
.then(res => {
return res.json()
})
.then(res => {
const arrayData = [...this.state.data, ...res]
this.setState({
data: arrayData,
refreshing: false
});
})
.catch(error => {
console.log(error);
this.setState({ refreshing: false });
});
};
handleRefresh = () => {
this.setState(
{
refreshing: true
},
() => {
this.fetchData();
alert('Pulled Up to Refresh');
}
);
};
render() {
return (
<View>
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
horizontal={false}
data={this.state.data}
keyExtractor={item => item.id}
renderItem={({ item }) =>
<View>
<Text>{item.title.rendered}</Text>
</View>
}
/>
</View>
);
}
}
When I pull up to refresh I get this warning: Two children with same key. Keys should be unique. This is weird because each post ID is unique. And even with this warning, the new posts that are in API don't show unless I re-load the application.
Change your handleRefresh function like below:
handleRefresh = () => {
this.setState(
{
refreshing: true,
data:[]
},
() => {
this.fetchData();
alert('Pulled Up to Refresh');
}
);
};