React Native. Flatlist order changes when updating item - react-native

Click here for Example
Hello all.
I have a small issue with a React Native Flatlist.
It takes the items from Redux store, but when i try to update the amount, with redux as well it changes me the order of the list. Except in last item.
If you encountered similar issue let me know.
Thanks in advance
So far i discovered that the increase and decrease functions in Cart Item changes the order of my array.
These are the components i'm using to render the list.
CART LIST
import React from "react";
import { FlatList } from "react-native";
import CartItem from "./CartItem";
const CartList = ({ data }) => {
return (
<React.Fragment>
{data && (
<FlatList
showsVerticalScrollIndicator={false}
data={data}
renderItem={({ item }) => <CartItem item={item} />}
keyExtractor={(item) => console.log(item)}
/>
)}
</React.Fragment>
);
};
export default CartList;
CART ITEM
import React, { useCallback } from "react";
import { View, Text, StyleSheet, Image } from "react-native";
import IncreaseDecrease from "./IncreaseDecrease";
// Theme
import { Theme } from "../../Theme";
const colors = Theme.colors;
// Redux
import { useDispatch, useSelector } from "react-redux";
import {
updateQuantity,
removeFromCart,
} from "../../Redux/Actions/cartActions";
const CartItem = React.memo(({ item }) => {
const dispatch = useDispatch();
const { cart } = useSelector((state) => ({
cart: state.cart.cart,
}));
const increaseQuantity = useCallback(
() => {
let currentProduct = cart.filter((el) => el.product.id === item.product.id);
let quantity = currentProduct[0].quantity + 1;
dispatch(updateQuantity(item.product.id, quantity));
},
[],
);
const decreaseQuantity = useCallback(
() => {
let currentProduct = cart.filter((el) => el.product.id === item.product.id);
let quantity = currentProduct[0].quantity - 1;
if (quantity == 0) {
dispatch(removeFromCart(item.product.id));
} else {
dispatch(updateQuantity(item.product.id, quantity));
}
},
[],
);
return (
<React.Fragment>
{item && (
<View style={styles.container}>
<View style={styles.imageContainer}>
<Image style={styles.image} source={{ uri: item.product.image }} />
</View>
<View style={styles.textContainer}>
<Text
style={[styles.text, { fontSize: 16, color: colors.darkGrey }]}
>
{item.product.name}
</Text>
</View>
<View style={styles.quantity}>
<IncreaseDecrease
quantity={item.quantity}
increase={increaseQuantity}
decrease={decreaseQuantity}
/>
</View>
<View style={styles.priceContainer}>
<Text
style={[styles.text, { fontSize: 16, color: colors.darkGrey }]}
>
${item.product.price * item.quantity}
</Text>
</View>
</View>
)}
</React.Fragment>
);
});
INCREASE DECREASE
import React from "react";
import { View, StyleSheet, Text } from "react-native";
import PropTypes from "prop-types";
import Icon from "react-native-vector-icons/Ionicons";
// Theme
import { Theme } from "../../Theme";
import { TouchableOpacity } from "react-native";
const colors = Theme.colors;
// Types
const propTypes = {
quantity: PropTypes.number,
increase: PropTypes.func,
decrease: PropTypes.func,
};
// Default Props
const defaultProps = {
quantity: 0,
};
const IncreaseDecrease = ({ quantity, increase, decrease }) => {
return (
<View style={styles.container}>
<TouchableOpacity onPress={decrease}>
<View style={styles.button}>
<Icon name={"remove"} size={20} color={colors.white} />
</View>
</TouchableOpacity>
<View style={styles.input}>
<Text>{quantity}</Text>
</View>
<TouchableOpacity onPress={increase}>
<View style={styles.button}>
<Icon name={"add"} size={20} color={colors.white} />
</View>
</TouchableOpacity>
</View>
);
};
REDUCER:
case UPDATE_QUANTITY:
let item = cart.find(
(item) => item.product.id == action.payload.productId
);
let newCart = cart.filter(
(item) => item.product.id != action.payload.productId
);
item.quantity = action.payload.quantity;
newCart.push(item);
return {
...state,
cart: newCart,
};

Okay i got it.
The issue was in my reducer, i was pushing a new updated element to a new array.
The solution is to change the element and update through the index.
Here is my solution:
REDUCER:
case UPDATE_QUANTITY:
let item = cart.find(
(item) => item.product.id == action.payload.productId
);
let index = cart.indexOf(item)
item.quantity = action.payload.quantity;
let newCart = cart.slice()
newCart[index] = item
return {
...state,
cart: newCart,
};

In your reducer what you are doing is finding the item whose quantity is to be updated. Update its quantity and push that item to the end of the list.
Hence changing the order of items in the cart.
Instead, you should try to find the index of the item to be updated, replace the quantity property in the cart object at that index with a new one and the order of the list will be preserved.
Try this -
case UPDATE_QUANTITY:
let itemIndex = cart.findIndex(
(item) => item.product.id == action.payload.productId
);
let newCart = [...state.cart];
newCart[itemIndex].quantity = action.payload.quantity;
return {
...state,
cart: newCart,
};

Related

How does onBlur event work in react native? it doesn't work i want it to

I made an expo app.
I want to make an AutoCompleteInput.
When focusing and changing TextInput, It will render the same words.
When ListItem clicks, TextInput's value will update it and it doesn't render.
When TextInput focus out, ListItem doesn't render.
In my code, the onPress event works first. onPress event works after.
how can i do?
and how does work onBlur event?
i moved onBlur event other component, but it doesn't work...
// App.js
...
return (
<View style={styles.container}>
<View style={styles.contentContainer}>
<Text>my content</Text>
</View>
<AutoInput />
<StatusBar style="auto" />
</View>
);
// AutoInput.jsx
import { ListItem, TextInput } from '#react-native-material/core';
import { useCallback, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { locations } from '../../utils/constants';
const initialLocation = locations.map(({ name }) => name);
const AutoInput = () => {
const [location, setLocation] = useState('');
const [searchedLocations, setSearchedLocations] = useState([]);
const handleChangeInputText = useCallback(
(newString) => {
setLocation(newString);
if (!newString) {
setSearchedLocations(initialLocation);
return;
}
const filteredLocations = locations
.filter(({ name }) => name.includes(newString))
.map(({ name }) => name);
setSearchedLocations(filteredLocations);
},
[initialLocation]
);
return (
<View style={styles.container}>
<TextInput
placeholder="지역을 검색해주세요"
onBlur={() => {
console.log('blur');
setSearchedLocations([]);
}}
onFocus={() => console.log('focus')}
onChangeText={handleChangeInputText}
value={location}
/>
<ScrollView style={styles.scroll}>
{searchedLocations.map((searchLocation) => (
<ListItem
key={searchLocation}
title={searchLocation}
onPress={() => alert(searchLocation)}
style={styles.listItem}
/>
))}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: { position: 'absolute', width: '100%' },
scroll: { maxHeight: 300 },
listItem: { width: '100%' },
});
export default AutoInput;
enter image description here

React Native useState not auto Updating?

why useState auto update? I'll press button and not showing textinput. but I can save file without change. textinput will be showing. sorry my bad english
import React, { useState,useEffect } from 'react';
import {Text, TextInput, View, Button,} from 'react-native';
const Test = ({navigation}) => {
const [textInput, settextInput] = useState([]);
useEffect(() => {
addTextInput = (key) => {
textInput.push([<TextInput style={{backgroundColor :'#7ACB4A',marginTop:10}} key={key} />]);
settextInput(textInput);
console.log(textInput);
}
},[textInput]);
return(
<View>
<Button title='+' onPress={() =>
addTextInput(textInput.length)} />
{textInput.map((value, index) => {
return value
})}
<Text>{textInput.length}</Text>
</View>
);
}
export default Test;
I have a few suggests to make your code better.
Don't change state value if not in use 'setState'.
This is false by nature and causes errors.
addTextInput = (key) => {
textInput.push([<TextInput style={{backgroundColor :'#7ACB4A',marginTop:10}} key={key} />]);
settextInput(textInput);
console.log(textInput);
}
State merely contains value, it should not contain different things. You should return TextInput in your map function.
I try rewrite your code, sorry because my english. Hope help you
code:
const [textInput, setTextInput] = React.useState(['1', '2'])
const addTextInput = (key: string) => {
const tempInput = textInput.concat([key])
setTextInput(tempInput)
}
return (
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<Button title="+" onPress={() => addTextInput(textInput.length.toString())} />
{textInput.map((value, index) => {
return (
<TextInput style={{ backgroundColor: '#7ACB4A', marginTop: 10, width: '70%' }} key={index + ''} />
)
})}
<Text>{textInput.length}</Text>
</View>
)
}

onPress not working in React Native Flatlist

My onPress handler is not working when someone clicks on Flatlist item.
Video of this issue
https://u.pcloud.link/publink/show?code=XZWGOUkZmDLPeKQOQJJzxnqFB8Q21X3acT7k
Here is the code:
import React, { useState, useEffect } from 'react';
import { View, Text, Image, FlatList, ActivityIndicator } from 'react-native';
import { TouchableNativeFeedback } from 'react-native-gesture-handler';
import axios from 'axios';
export default function _AjaxApp() {
const [postList, setPostList] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const loadData = (append = false) => {
let url = "https://edristi.in/wp-json/wp/v2/posts?per_page=20&page=" + currentPage;
setIsLoading(true);
setCurrentPage(currentPage + 1);
axios.get(url).then((r) => {
if (append) {
setPostList(postList.concat(r.data));
} else {
setPostList(r.data);
}
setIsLoading(false);
}).catch((e) => {
console.log(e);
});
}
useEffect(() => {
loadData();
}, [])
let Loader = <></>
if (isLoading) {
Loader = <ActivityIndicator></ActivityIndicator>
}
return (
<View>
<View style={{padding:20, backgroundColor:"#4342fe"}}>
<Text style={{color:"white"}}>Edristi App</Text>
</View>
<FlatList
data={postList}
renderItem={({ item, index, separators }) => <PostCard postList={postList} {...item} index={index} />}
keyExtractor={r => r.id + "-" + Math.random().toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={2}
ListFooterComponent={Loader}
onEndReachedThreshold={0.5}
onEndReached={() => {
loadData(true);
}}
/>
</View>
);
}
class PostCard extends React.PureComponent {
onPressHandler() {
console.log("Clicked");
alert("Clicked");
}
render() {
let image = <></>
if (this.props.jetpack_featured_media_url.trim() !== "") {
image = <Image style={{ flex: 1 }} source={{
//uri: this.props.featuredimage,
uri: this.props.jetpack_featured_media_url,
}} />
}
// console.log(this.props.jetpack_featured_media_url);
return <TouchableNativeFeedback onPress={()=>{
this.onPressHandler();
}}>
<View style={{ margin: 10 }}>
<Text style={{ fontSize: 17, lineHeight: 23, fontWeight: "600" }}>{ this.props.title.rendered}</Text>
</View></TouchableNativeFeedback>
}
}
Try to import 'TouchableNativeFeedback' from 'react-native' instead of 'react-native-gesture-handler'.

React Native: how to handle state for each item in a rendered FlatList when pressing the 'like' button?

I'm trying to handle the state for a 'heart' icon in a rendered Flat List (which loads data from Firebase) for each individual item within the Flat List.
The code works, in that the heart icon fills in and the data is pushed to the database when the icon is pressed. Likewise, pressing the heart icon again reverts the icon and removes the 'like' from the database.
However, when the heart icon is clicked, it swaps between the filled in state and hollow state for the heart icon for every item in the list, when I'm trying to alter state for that specific item.
I understand that I need to handle state locally for each item in the Flat List, but I've no idea how to do it. Any help would be appreciated. Code below:
import React, {Component} from 'react';
import {
FlatList,
Text,
View,
} from 'react-native';
import {Icon} from 'react-native-elements';
import {globalStyles} from '../config/Styles';
import Firebase from 'firebase';
import 'firebase/database';
export default class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
//set value of postList variable as an empty array
postList: [],
liked: false,
};
}
componentDidMount() {
this.getPostData();
}
getPostData = () => {
const ref = Firebase.database().ref('/posts');
ref.on('value', snapshot => {
const postsObject = snapshot.val();
if (!postsObject) {
console.log('NO DATA IN FIREBASE:', Date(Date.now()));
} else {
console.log('HOMESCREEN FIREBASE DATA RETRIEVED:', Date(Date.now()));
const postsArray = Object.values(postsObject);
this.setState({postList: postsArray});
}
});
};
render() {
return (
<View>
<FlatList
keyExtractor={post => post.id}
data={this.state.postList}
renderItem={({item: post}) => (
<View style={globalStyles.postContainer}>
<Text style={globalStyles.postText}>
{post.heading}
{'\n'}#{' '}
<Text style={{fontWeight: 'bold'}}>{post.location}</Text>
{'\n'}
{post.description}
{'\n'}
listed by{' '}
<Text style={{fontWeight: 'bold'}}>{post.createdBy}</Text>
{'\n'}
on <Text style={{fontWeight: 'bold'}}>{post.createdAt}</Text>
</Text>
<View style={globalStyles.iconMargin}>
<Icon
raised
iconStyle={globalStyles.icon}
name={this.state.liked ? 'heart' : 'heart-o'}
size={28}
type="font-awesome"
onPress={() => {
const userKey = Firebase.auth().currentUser.uid;
const postKey = post.id;
const favRef = Firebase.database().ref(
'favourites/' + userKey + '/' + postKey,
);
if (this.state.liked === false) {
favRef.set({
id: postKey,
heading: post.heading,
description: post.description,
location: post.location,
createdAt: post.createdAt,
createdBy: post.createdBy,
});
this.setState({liked: true});
} else {
favRef.remove();
this.setState({liked: false});
}
}}
/>
<Icon
raised
iconStyle={globalStyles.icon}
name="flag-o"
size={28}
type="font-awesome"
onPress={() =>
this.props.navigation.navigate('ReportPostScreen', post)
}
/>
</View>
</View>
)}
/>
</View>
);
}
}
Ok so the issue is that you've got a singular liked state value instead of an array. You should firstly change liked to an array (which will store the id of the posts which are liked). Maybe call it something more appropriate such as likePosts. Then you can add or remove post ids from the array when they're liked or unliked (and check the likedPosts array for the value when deciding what icon to display).
Your modified code should look something like this:
import React, {Component} from 'react';
import {
FlatList,
Text,
View,
} from 'react-native';
import {Icon} from 'react-native-elements';
import {globalStyles} from '../config/Styles';
import Firebase from 'firebase';
import 'firebase/database';
export default class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
//set value of postList variable as an empty array
postList: [],
likedPosts: [],
};
}
componentDidMount() {
this.getPostData();
}
getPostData = () => {
const ref = Firebase.database().ref('/posts');
ref.on('value', snapshot => {
const postsObject = snapshot.val();
if (!postsObject) {
console.log('NO DATA IN FIREBASE:', Date(Date.now()));
} else {
console.log('HOMESCREEN FIREBASE DATA RETRIEVED:', Date(Date.now()));
const postsArray = Object.values(postsObject);
this.setState({postList: postsArray});
}
});
};
render() {
return (
<View>
<FlatList
keyExtractor={post => post.id}
data={this.state.postList}
renderItem={({item: post}) => (
<View style={globalStyles.postContainer}>
<Text style={globalStyles.postText}>
{post.heading}
{'\n'}#{' '}
<Text style={{fontWeight: 'bold'}}>{post.location}</Text>
{'\n'}
{post.description}
{'\n'}
listed by{' '}
<Text style={{fontWeight: 'bold'}}>{post.createdBy}</Text>
{'\n'}
on <Text style={{fontWeight: 'bold'}}>{post.createdAt}</Text>
</Text>
<View style={globalStyles.iconMargin}>
<Icon
raised
iconStyle={globalStyles.icon}
name={this.state.likedPosts.indexOf(post.id) > -1 ? 'heart' : 'heart-o'}
size={28}
type="font-awesome"
onPress={() => {
const userKey = Firebase.auth().currentUser.uid;
const postKey = post.id;
const favRef = Firebase.database().ref(
'favourites/' + userKey + '/' + postKey,
);
// This checks that the array doesn't contain the post id (i.e. the post was not previously liked)
if (this.state.likedPosts.indexOf(post.id) === -1) {
favRef.set({
id: postKey,
heading: post.heading,
description: post.description,
location: post.location,
createdAt: post.createdAt,
createdBy: post.createdBy,
});
// Include the post.id in the likedPosts array
this.setState({ likedPosts: [...this.state.likedPosts, post.id] })
} else {
favRef.remove();
// Remove the post.id from the likedPosts array
let index = this.state.likedPosts.indexOf(post.id);
this.setState({ likedPosts: this.state.likedPosts.splice(index, 1) })
}
}}
/>
<Icon
raised
iconStyle={globalStyles.icon}
name="flag-o"
size={28}
type="font-awesome"
onPress={() =>
this.props.navigation.navigate('ReportPostScreen', post)
}
/>
</View>
</View>
)}
/>
</View>
);
}
}
becuase this.state.liked will be true for all items in the json respone
to correct it you can update the state array json
ItemPRessed =(index)=>{let dataArray = this.state.data
dataArray[index].liked = !dataArray[index].liked
this.setState({
data:dataArray
})}
and instead of this.state.liked use post.liked so it will be specific to the item
and instead of this.setState({liked: true});
put
this.ItemPRessed(Index)
i don't know how your indexs work in your json put if it is like this
[{item},{item}]
then you can use renderItem=({item: post, index}) instead of renderItem={({item: post})
to get the index on which item it is pressed then

How to highlight search results in React-Native FlatList?

I'm newbie in React-native. I need to highlight the search results in my FlatList while I'm typing in search bar. There are 2 componenrs: react-native-highlight-words and react-native-text-highlight , But I cant figure out how to make use of them!
here is my code:
import React, { Component } from 'react';
import {StyleSheet, Text, View, FlatList, TouchableOpacity } from 'react-native';
import { List, ListItem, SearchBar } from 'react-native-elements';
import DropdownMenu from 'react-native-dropdown-menu';
import {Header, Left, Right, Icon} from 'native-base'
var SQLite = require('react-native-sqlite-storage')
var db = SQLite.openDatabase({name: 'test.sqlite', createFromLocation: '~dictionary.sqlite'})
var data = [["English", "Arabic", "Persian"]];
export default class App extends Component {
constructor(props) {
super(props)
this.state = {record: [], arrayholder : [], query:''};
db.transaction((tx) => {
tx.executeSql('SELECT * FROM tblWord', [], (tx, results) => {
let row = results.rows.item();
arrayholder = results.rows.raw()
record = results.rows.raw()
this.setState({arrayholder: arrayholder})
this.setState({ record: record })
}});});
}
searchFilterFunction = text => {
var newData = this.state.arrayholder;
newData = this.state.arrayholder.filter(item => {
const itemData = item.word_english.toLowerCase()
const textData = text.toLowerCase()
return itemData.indexOf(textData) > -1 });
this.setState({query: text,record: newData });
};
render() {
return (
<View style = {styles.container}>
<Header style={styles.headerStyle}>
...
</Header>
<View style={styles.menuView}>
<DropdownMenu
bgColor={"#B38687"}
activityTintColor={'green'}
titleStyle={{color: '#333333'}}
handler={(selection, row) => this.setState({text4: data[selection][row]})}
data={data}
>
</DropdownMenu>
</View >
<View >
<View style={styles.searchBarView}>
<SearchBar
placeholder="Search"
lightTheme
value = {this.state.query}
onChangeText={text => this.searchFilterFunction(text)}
autoCorrect={false}
inputStyle={{backgroundColor: 'white'}}
containerStyle={{backgroundColor: 'white', borderWidth: 1, borderColor:'#B38687', }}
/>
</View>
<View style={styles.flatListVew}>
<List containerStyle={{ flexDirection: 'column-reverse', borderTopWidth: 0, borderBottomWidth: 0 }} >
<FlatList
data={this.state.record}
keyExtractor={((item, index) => "itemNo-" + index)}
renderItem={({item}) => (
<ListItem
roundAvatar
onPress={() => {this.props.navigation.navigate('Screen2', {data: (item.word_english +'\n' + item.word_arabic)} ); }}
title={item.word_english}
containerStyle={{ borderBottomWidth: 0 }}
/> )}
/>
</List>
</View>
</View>
</View>);}
}
const styles = StyleSheet.create({
...
I want the results to look like this:
Any help would be greatly appreciated.
You can pass text or custom view to ListItem component as props for title. I am using React Native Highlight Words to highlight text as you stated.
add React Native Highlight Words by add the below line:
import Highlighter from 'react-native-highlight-words';
Update code for ListItem component for desired result:
<ListItem
roundAvatar
onPress={() => {this.props.navigation.navigate('Screen2', {data: (item.word_english +'\n' + item.word_arabic)} ); }}
title={
<Highlighter
highlightStyle={{backgroundColor: 'yellow'}}
searchWords={[this.state.query]}
textToHighlight={item.word_english}
/>}
containerStyle={{ borderBottomWidth: 0 }}
/>
You can highlight using your own styles.
Here is a simple example:
const myList = [{ text: 'Hi', id: 1 }, ... ]
class List extends Component {
this.state = { highlightedId: undefined }
render() {
return (
<FlatList
data={myList}
renderItem={({item}) => <Text style={item.id === this.state.highlightedId ? styles.hightlighted : undefined}>{item.text}</Text>}
/>
)
}
}
const styles = StyleSheet.create({
highlighted: {
backgroundColor: "yellow"
}
})
In your case you can adjust the containerStyle of the <ListItem />.
handleChangeText = param => {
const {categoryList} = this.state;
const regEx = "\\s*(" + param + ")\\s*"
const validator = new RegExp(regEx, 'i');
let filterData = [];
//here is categorylist is the data supplied to flatlist.
categoryList.forEach(item => {
let flag = validator.test(item.teamtype);
if (flag) {
//here set the highlighting color
}
})
};
Call the above function onChangeText() of your search field.