I've build an app in react-native and i'm using react-redux to store data in my cart.
Currently when I add a product lets say apple to my cart. My cart will contain 1x apple and if I add the same product to my cart I will have 2x apple.
Problem
But if I try to remove 1x apple my cart removes all the apples in my cart.
Question
How can I remove the item in my cart by -1?
cartitems.js
case 'REMOVE_FROM_CART':
return state.filter(cartItem=>cartItem.id !==
action.payload.id )
CartProducts.js component
<View key={products.index} style={styles.appels}>
<View style={styles.iconContainer}>
<Icon name={products.item.icon} color="#DD016B" size={25} />
</View>
<View style={styles.text}>
<Text style={styles.name}>
{products.item.name}
</Text>
<Text style={styles.price}>
€ {products.item.price}
</Text>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity onPress={() => this.props.onPress(products.item)} >
<Icon style={styles.button} name="ios-remove" color="white" size={25} />
</TouchableOpacity>
</View>
</View>
CartScreen.js
<Products products={appels} onPress={this.props.addItemToCart}/>
CartScreen.js | handeling removing product from cart
const mapDispatchToProps = (dispatch) =>{
return{
removeItem:(product) => dispatch ({
type:'REMOVE_FROM_CART' , payload: product
})
}
}
I believe you are keeping duplicates of the same product in the state when multiple copies of the same product are added. You should therefore only filter the first match instead of all matches:
case 'REMOVE_FROM_CART': {
const index = state.findIndex(item => item.id === action.payload.id);
return state.filter((_, i) => i !== index);
}
This should get it to work while keeping your current design.
However, if the user removes a duplicated product from the bottom of the list, the UI might not be intuitive as the first copy of the product in the list will be removed from the UI instead, hence you might want to adjust your removeItem action payload to contain the index of the product to be removed instead of the product object.
A better approach might be to use a qty variable to indicate multiple copies, which will reduce the state size and make it easier to display a quantity column on the cart screen (instead of displaying duplicated products):
case 'ADD_ITEM_TO_CART':
if (state.some(item => item.id === action.payload.id)) {
// increase qty if item already exists in cart
return state.map(item => (item.id === action.payload.id ? { ...item, qty: item.qty + 1 } : item));
}
return [...state, { ...action.payload, qty: 1 }]; // else add the new item to cart
case 'REMOVE_FROM_CART':
return state
.map(item => (item.id === action.payload.id ? { ...item, qty: item.qty - 1 } : item))
.filter(item => item.qty > 0);
Your CartScreen.js will also need to be modified accordingly if you use this new design.
When doing
return state.filter(cartItem=>cartItem.id !== action.payload.id )
You are basically saying, return everything but this cartItem specificaly.
That is not what you want. You want to give :
A quantity to remove in the payload.
The id of the product.
So here is your action :
dispatch({type:'REMOVE_FROM_CART' , payload: {product, qty} })
Then you have to find your item and update it's quantity :
case 'REMOVE_FROM_CART':
const updatedProductList = state.productList.map( (product) => {
if(product.id === payload.product.id){ // this is the item we care about
return {
...product, // this will avoid a state mutation
qty : product.qty - payload.qty // update the qty
}
}else { // just return the element as it is
return cartItem
}
})
return updatedCartItemList;
break;
default :
return state
case UNFAVORITE: {
return {
favorite: [
...state.favorite.filter(favorite => favorite !== action.payload),
],
};
}
simply do this to UNFAVORITE
Well, you may need to pass either quantity or increment/decrement flag in your action payload.
your action creator may accept parameter for increment/decrement
const updateItem = dispatch=>(increment=true)=>{
dispatch({type:'UPDATE_IN_CART' , payload: product, increment})
}
Now in your reducer
case 'UPDATE_IN_CART':{
const {increment, payload}= action; //ES6 destructing
const updatedCart = state.map((cartItem)=>{
if(cartItem.id !== payload.id )){
cartItem.quantity = increment?cartItem.quantity+1:cartItem.quantity-1 //will handle increment/decrement;
}
return cartItem;
})
return updatedCart;
}
Related
I'm using useEffect to retrieve some user and group data on initial screen load on a react-native app. The following code for this is here:
const [groupInfo, setGroupInfo] = useState([]);
//Called on INITIAL rendering
useEffect(() => {
async function getGroupData() {
let groupCode = '';
//Retrieve group code from user
await getDoc(doc(db, 'users', email)).then(userSnapshot => {
if (userSnapshot.exists()) {
groupCode = userSnapshot.data()['group_code'];
}
else { console.log('No user with that email exists!'); }
}).catch(err => {
console.log(err);
});
//Retrieve group information from user
await getDoc(doc(db, 'groups', groupCode)).then(groupSnapshot => {
if (groupSnapshot.exists()) {
setGroupInfo(groupSnapshot.data());
}
else { console.log('No group with that code exists!'); }
}).catch(err => {
console.log(err);
});
}
getGroupData();
}, [email]);
The problem is that when I've tried to render this on my return statement, I get an error. I've logged my data before and that has worked fine but it seems that the app loads the view first. THe following react render code and error are below:
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerLeft}>Goals</Text>
<Text style={styles.headerRight}>Week of 11/20/22</Text>
</View>
<Text style={styles.prize}>Prize: Winner gets a free starbucks drink!</Text>
{/*<View style={styles.goalBox}>
<Text>Workout 3x per week</Text>
<CheckBox style={styles.checkbox}/>
</View> */}
{/* TODO edit prize screen */}
<Button title="Edit prize" />
<View style={styles.memberHeader}>
<Text style={styles.headerLeft}>Members</Text>
<Text style={styles.numMembers}>4</Text>
</View>
{
groupInfo['members'].map((memberName, index) =>
<View style={styles.member}>
<Text style={styles.memberText}>{memberName}</Text>
</View>
)
}
<Button title="Invite member" />
</View>
);
Error:
ERROR TypeError: Cannot read property 'map' of undefined
This error is located at:
in ScreenViewGroup (created by SceneView)
EDIT:
So I think the problem is that at the same time the component is being rendered, the data is being loaded in. I'm still receiving the same error but I've noticed that when I edit my code, the data gets loaded in automatically.
Text strings also seem to render in properly as well but just not the array.
So I found a work-around. By declaring my initial array with values, React will fill those values in as default values and immediately change them to the firebase values when they load.
I'm building a barcode reader app that scans that qr code and then takes data and is used as a key to fetch an object from firebase. In order the data to be used as a key I need to pass through another screen but when I check console log it's cameback that the scanned key is undefined.
The itself barcode scanner works perfectly.
Barcode class :
export class BarCodeScannerScreen extends Component{
state = {
CameraPermissionGranted: null,
}
async componentDidMount() {
// Ask for camera permission
const { status } = await Permissions.askAsync(Permissions.CAMERA);
this.setState({ CameraPermissionGranted: status === "granted" ? true : false });
};
barCodeScanned = ({ data }) => {
//Access the Data
alert(data); // shows the scanned key
this.props.navigation.navigate('Info', {
item: data, }); // but then it's dissapears in here.
};
render(){
const { CameraPermissionGranted } = this.state;
if(CameraPermissionGranted === null){
// Request Permission
return(
<View style={styles.container}>
<Text>Please grant Camera permission</Text>
</View>
);
}
if(CameraPermissionGranted === false){
// Permission denied
return (
<View style={styles.container}>
<Text>Camera Permission Denied.</Text>
</View>
);
}
if(CameraPermissionGranted === true){
// Got the permission, time to scan
return (
<View style = {{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<BarCodeScanner
onBarCodeScanned = {this.barCodeScanned }
style = {{
height: DEVICE_HEIGHT/1.1,
width: DEVICE_WIDTH,
}}
>
</BarCodeScanner>
</View>
);
}
}
}
Here is my Info screen that receives the information :
export default class InfoScreen extends Component {
constructor(props){
super(props);
this.state={
productlist:[],
scannedkey: this.props.route.params.item
} }
async componentDidMount(){
firebase.database().ref(`product/${ this.state.scannedkey}`).on(
"value",
(snapshot) => {
var list = [];
snapshot.forEach((child) => {
list.push({
key: child.key,
title: child.val().title,
//details: child.val().details,
//price: child.val().price
});
});
this.setState({ productlist: list });
},
(error) => console.error(error)
);
}
componentWillUnmount() {
if (this.valuelistener_) {
this.valueRef_.off("value", this.valuelistener_)
}}
render() {
console.log(this.state.scannedkey); // console log shows that scanned key is undefined
return(
<View style={styles.container}>
<Text>Hey</Text>
<Text>{this.state.productlist.title}</Text>
</View>
);}}
App.js
export default function App() {
const Drawer=createDrawerNavigator();
return (
<Provider store={store}>
<NavigationContainer>
<Drawer.Navigator initialRouteName="Barcode">
<Drawer.Screen name="Barcode" component={BarCodeScannerScreen} />
<Drawer.Screen name="Info" component={InfoScreen} />
</Drawer.Navigator>
</NavigationContainer>
</Provider>
);
}
I ussualy use function components to navigate through but with class components it's a little tricky for me. Perhaps I missed something?
So far I 've tried :
this.props.navigation.navigate('Info', {
item: JSON.stringify(data) , });
And it didn't work.
I will be grateful for your help.
Try to use item directly from props, not from state
in your componentDidMount call where you supply from state the scannedKey, supply it from props
firebase.database().ref(`product/${this.props.route.params.item}`)....
you are also calling this.props instead of props directly in your state inside your constructor, which have direct access to it, that's why you can call super(props) and not super(this.props), I am not sure if this is the issue, but in react docs says don't copy props to state because they get ignored, and it's bad practice my friend.
check this link, in the big yellow note what I am reffering to
https://reactjs.org/docs/react-component.html#constructor
I have a structure of data i want to fetch and display in a Flat List here is my data.
-Lt10FlMt0xtru36Ztmb
name : "Hello"
-Lt0pdC5Ikwd-ZWNBiYJ
name : "Coke"
-Lt0paPi_-zkCelfoisM
name : "Pespsi"
Here is my code:
readUserData = () => {
firebase.database().ref('Brands/').once('value').then((snapshot) => {
console.log(snapshot.val())
const userdata = snapshot.val();
// Alert.alert("Helo" , userdata);
this.setState({
getListbrands : userdata
})
Alert.alert("Data" , JSON.stringify(this.state.getListbrands))
});
}
and here i am setting data in Flat list but it is not showing any thing please guide.
{
this.state.getListbrands &&
<FlatList
data={this.state.getListbrands}
keyExtractor={(a, b) => b.toString()}
renderItem={({ item }) => (
<Text style={{color:'#000'}}>{item.name}</Text>
)}
/>
}
According to the documentation, FlatList component expects an array of data. The value of your snapshot is an object, and not an array.
If you want to use an array of the values of each child node, you can get that with:
firebase.database().ref('Brands/').once('value').then((snapshot) => {
userdata = [];
snapshot.forEach((child) {
userdata.push(snapshot.val());
})
...
You can then set this array to the FlatList.
{
this.state.getListbrands &&
<FlatList
data={this.state.getListbrands}
keyExtractor={(a, b) => b.toString()}
renderItem={(item) => (
<Text style={{color:'#000'}}>{item.name}</Text>
)}
/>
}
change like this.i think item in here is not a object.please try. it will work. thank you.
I am making a react app, I have a "categories" collection and a presentation layer with a button to remove a category. My redux state refresh if I reload the page otherwise is persistent. I don't understand why it is not working since seems immutable to me, plus I am using the same pattern in another component and works fine. What am I missing ?
Here the reducer:
import {DELETE_CATEGORY} from './../../actionType';
const initialState = {
singleCategory: {},
categories: []
}
export default function(state = initialState, action){
switch(action.type){
case DELETE_CATEGORY:
return {
...state,
categories: state.categories.filter (category=>category._id !== category.payload)
}
default:
return state;
}
}
By the way if I refresh the page the store is filtered correctly.
Here is my action:
// Delete Category by id
export const deleteCategory = (categoryId) => dispatch => {
console.log('disp');
axios
.delete(`/api/categories/${categoryId}`)
.then( success => {
dispatch(successMessage);
return dispatch({
type: DELETE_CATEGORY,
payload: categoryId
})
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
}),
);
};
Here is my component:
class ShowCategoriesPage extends Component {
componentDidMount(){
this.props.getCategories()
}
handleDeleteCategory = (categoryId) => {
this.props.deleteCategory(categoryId);
}
render() {
const { categories } = this.props.categories;
return (
<SidebarLayout>
{categories.map(category => (
<CategoryCard
name={category.name}
type={category.type}
id={category._id}
handleDeleteCategory={this.handleDeleteCategory}
/>
))}
<Button variant="contained" color="primary" style={{marginTop: '36px'}} >
ADD NEW CATEGORY
</Button>
</SidebarLayout>
)
}
}
and this is the component:
const SimpleCard = ({ classes, name, type, id, deleteCategory })=>{
const onDeleteCategory = (categoryId) => {
deleteCategory(categoryId);
}
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography className={classes.pos} color="textSecondary">
{type}
</Typography>
<Button variant="small" color="primary" style={{marginTop: '36px'}} onClick={()=>onDeleteCategory(id)}>
REMOVE
</Button>
<Button variant="small" color="primary" style={{marginTop: '36px'}} >
EDIT
</Button>
</CardContent>
</Card>
);
}
I read the official redux documentation and it advise to use splice or filter like I did, can you understand why redux state is not refreshing clicking on delete button?
I want to avoid to force refresh the state.
are you sure this line is correct
categories: state.categories.filter (category=>category._id !== category.payload)
and not
categories: state.categories.filter (category=>category._id !== action.payload)
I am using React Native 0.49. I have data fetched from firebase, list of users users/, each item in this list was set like this firebase.database().ref('users/' + userId).set(userInfo) userId is the uid of the currentUser.
Now I am fetching back (in actions - redux):
export function getPeople(){
return (dispatch) => {
dispatch(getPeopleRequest());
getData().then(result => {
dispatch(getPeopleSuccess(result.val()))
})
.catch(error => {
dispatch(getPeopleFailure(error))
});
}
}
const getData = () => {
const userRef = firebase.database().ref('users/').limitToFirst(20);
return userRef.once('value');
}
In component, I am trying to render the data in FlatList, but it's not rendering anything, I don't know what I'm doing wrong:
componentDidMount(){
this.props.getPeople();
}
_renderItem = ({ item }) => (
<View style={{flex: 1}}>
<Text>{item}</Text>
</View>
);
render(){
const { inProgress, people, error } = this.props.peopleData;
return (
<FlatList
data={people}
renderItem={this._renderItem}
/>
);
}
when console log people this is result:
{cpycwkz7exVBzmhaLEtHjMW66wn1: {…}, UbIITfUANmb63cYE2x7dZRZ0pfK2: {…}}
FlatList component expects its data prop to be an array. You are passing it as an Object. You can change it to an array of Objects. Then too in your _renderItem method the item will be an object and it can't be rendered straight away in <Text>, you have to extract a text value from the item object and than render it as: <Text>SOME_TEXT_NOT_AN_OBJECT</Text>
You can convert your people object to an array and pass it to the <FlatList like this:
render(){
const { inProgress, people, error } = this.props.peopleData;
let ArrayOfPeopleObject = Object.values(people)
return (
<FlatList
data={ArrayOfPeopleObject}
renderItem={this._renderItem}
/>
);
}
Now each item in the _renderItem method will be an object and you can extract value from any key and render it in the <Text>.
Flatlist data requires a key for each object in the array you can convert the firebase result like this:
Object.entries(peopleFromFirebase).map(item => ({...item[1], key: item[0]}));
So json from firebase like this:
{
cpycwkz7exVBzmhaLEtHjMW66wn1: {
name: 'wade owen watts',
phone:'+447...'
},
UbIITfUANmb63cYE2x7dZRZ0pfK2: {
name: 'Helen Harris',
phone:'+448...'
}
}
becomes:
[
{
key: 'cpycwkz7exVBzmhaLEtHjMW66wn1',
name: 'wade owen watts',
phone:'+447...'
},
{
key:'UbIITfUANmb63cYE2x7dZRZ0pfK2',
name: 'Helen Harris',
phone:'+448...'
}
]
Flat list except array of objects but the firebase return the data as map like {key: value} pair so you should transform this map to array , you can install lodash module and use _.values() function to do that