Upon the click of a single like, it is increasing the number of likes for both separate components. What is causing both like numbers to increase, and how can I code it to where only one like number increases upon clicking a like?
I have also include the console in the picture below where I have console logged the logic in my reducer. You can find the code for the reducer further below the picture.
Reducer code
import { GET_GOALS, GOAL_ERROR, UPDATE_LIKES } from "../actions/types";
const initialState = {
goals: [],
goal: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_GOALS:
return {
...state,
goals: payload,
loading: false
};
case GOAL_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_LIKES:
return {
...state,
goals: state.goals.map(goal =>
console.log("goal id", goal._id) === console.log("payload id", payload.goalId) ? { ...goal, likes: payload.likes } : goal
),
loading: false
};
default:
return state;
}
}
Action code
import axios from "axios";
import { GET_GOALS, GOAL_ERROR, UPDATE_LIKES } from "./types";
// Get goals
export const getGoals = () => async dispatch => {
try {
const res = await axios.get("/api/goal/goalfeed");
dispatch({
type: GET_GOALS,
payload: res.data
});
} catch (error) {
dispatch({
type: GOAL_ERROR,
payload: { msg: error.response }
});
}
};
// Add like
export const addLike = goalId => async dispatch => {
try {
const res = await axios.put(`/api/goal/like/${goalId}`);
dispatch({
type: UPDATE_LIKES,
payload: { goalId, likes: res.data }
});
} catch (error) {
dispatch({
type: GOAL_ERROR,
payload: { msg: error.response }
});
}
};
// Remove like
export const removeLike = goalId => async dispatch => {
try {
const res = await axios.put(`/api/goal/unlike/${goalId}`);
dispatch({
type: UPDATE_LIKES,
payload: { goalId, likes: res.data }
});
} catch (error) {
dispatch({
type: GOAL_ERROR,
payload: { msg: error.response }
});
}
};
Goals component code
import React, { useEffect } from "react";
import Moment from "react-moment";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import { addLike, removeLike } from "../../actions/goal";
import { getGoals } from "../../actions/goal";
import Spinner from "../layout/Spinner";
import Navbar from "../dashboard/Navbar";
import ThumbUpAltIcon from "#material-ui/icons/ThumbUpAlt";
import ThumbDownAltIcon from "#material-ui/icons/ThumbDownAlt";
import ChatIcon from "#material-ui/icons/Chat";
import DeleteIcon from "#material-ui/icons/Delete";
import DoneIcon from "#material-ui/icons/Done";
import {
Typography,
Container,
CssBaseline,
makeStyles,
Grid,
Avatar,
Paper,
Button
} from "#material-ui/core";
const useStyles = makeStyles(theme => ({
paper: {
height: "auto",
marginBottom: theme.spacing(3)
},
actionButtons: {
marginTop: "3vh"
},
profileHeader: {
textAlign: "center",
marginBottom: 20
},
avatar: {
width: theme.spacing(7),
height: theme.spacing(7)
}
}));
const Goals = ({
getGoals,
auth,
addLike,
removeLike,
goal: { goals, user, loading }
}) => {
useEffect(() => {
getGoals();
}, [getGoals]);
const classes = useStyles();
return loading ? (
<>
<Navbar />
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Spinner />
</div>
</Container>
</>
) : (
<>
<CssBaseline />
<Navbar />
<main>
<Container>
<Typography variant="h2" className={classes.profileHeader}>
Goals
</Typography>
{/* parent grid */}
<Grid container spacing={4}>
{goals.map(singleGoal => (
<Grid
className={classes.paper}
key={singleGoal._id}
spacing={1}
container
item
direction="row"
alignItems="center"
component={Paper}
>
<Grid
item
container
direction="column"
justify="center"
alignItems="center"
xs={3}
>
<Avatar className={classes.avatar} src={singleGoal.avatar} />
<Typography variant="caption">
{singleGoal.first_name} {singleGoal.last_name}
</Typography>
<Typography variant="caption" className={classes.postedOn}>
Posted on{" "}
<Moment format="MM/DD/YYYY">{singleGoal.date}</Moment>
</Typography>
</Grid>
<Grid container item direction="column" xs={9}>
<Typography variant="body1">{singleGoal.text}</Typography>
<Grid item className={classes.actionButtons}>
<Button size="small" onClick={e => addLike(singleGoal._id)}>
<ThumbUpAltIcon />
</Button>
<Typography variant="caption">
{singleGoal.likes.length}
</Typography>
<Button
size="small"
onClick={e => removeLike(singleGoal._id)}
>
<ThumbDownAltIcon />
</Button>
<Button href={`/goal/${singleGoal._id}`} size="small">
<ChatIcon />
</Button>
{!auth.loading && singleGoal.user === auth.user._id && (
<Button size="small">
<DoneIcon />
</Button>
)}
{!auth.loading && singleGoal.user === auth.user._id && (
<Button size="small">
<DeleteIcon />
</Button>
)}
</Grid>
</Grid>
</Grid>
))}
</Grid>
</Container>
</main>
</>
);
};
Goals.propTypes = {
getGoals: PropTypes.func.isRequired,
goal: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
goal: state.goal,
auth: state.auth
});
export default connect(mapStateToProps, { getGoals, addLike, removeLike })(
Goals
);
There exists a flaw in your conditional test.
state.goals.map(goal =>
console.log("goal id", goal._id) === console.log("payload id", payload.goalId) // What is this? it will always evaluate to true
? { ...goal, likes: payload.likes }
: goal
)
console.log('EQUAL?', console.log() === console.log()); // true
console.log('EQUAL?', console.log(3) === console.log(3)); // true
console.log('EQUAL?', console.log(3) === console.log('three')); // true
console.log('EQUAL?', console.log('apple') === console.log({})); // true
console.log('EQUAL?', console.log(42) === console.log(-42)); // true
The function console.log is a void return, i.e. undefined, so you are comparing undefined === undefined, which is always true.
console.log(undefined === undefined); // true
You are spreading in the new 'likes' value to every goal object.
Try instead:
state.goals.map(
goal => goal._id === payload.goalId
? { ...goal, likes: payload.likes }
: goal
)
Related
So here is my code:
VideoCall.jsx:
import React, { useState, useEffect } from "react";
import { config, useClient, useMicrophoneAndCameraTracks, channelName} from "./settings.js";
import { Grid } from "#material-ui/core";
import Video from "./Video.jsx";
import Controls from "./Controls";
export default function VideoCall(props) {
const { setInCall } = props;
const [users, setUsers] = useState([]);
const [start, setStart] = useState(false);
const client = useClient();
const { ready, tracks } = useMicrophoneAndCameraTracks();
useEffect(() => {
let init = async (name) => {
client.on("user-published", async (user, mediaType) => {
await client.subscribe(user, mediaType);
if (mediaType === "video") {
setUsers((prevUsers) => {
return [...prevUsers, user];
});
}
if (mediaType === "audio") {
user.audioTrack.play();
}
});
client.on("user-unpublished", (user, mediaType) => {
if (mediaType === "audio") {
if (user.audioTrack) user.audioTrack.stop();
}
if (mediaType === "video") {
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
}
});
client.on("user-left", (user) => {
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
});
try {
await client.join(config.appId, name, config.token, null);
} catch (error) {
console.log("error");
}
debugger;
if (tracks) await client.publish([tracks[0], tracks[1]]);
setStart(true);
};
if (ready && tracks) {
try {
init(channelName);
} catch (error) {
console.log(error);
}
}
}, [channelName, client, ready, tracks]);
return (
<Grid container direction="column" style={{ height: "100%" }}>
<Grid item style={{ height: "5%" }}>
{ready && tracks && (
<Controls tracks={tracks} setStart={setStart} setInCall={setInCall} />
)}
</Grid>
<Grid item style={{ height: "95%" }}>
{start && tracks && <Video tracks={tracks} users={users} />}
</Grid>
</Grid>
);
}
Video.jsx:
import { AgoraVideoPlayer } from "agora-rtc-react";
import { Grid } from "#material-ui/core";
import React, { useState, useEffect } from "react";
export default function Video(props) {
const { users, tracks } = props;
const [gridSpacing, setGridSpacing] = useState(12);
useEffect(() => {
setGridSpacing(Math.max(Math.floor(12 / (users.length + 1)), 4));
}, [users, tracks]);
return (
<Grid container style={{ height: "100%" }}>
<Grid item xs={gridSpacing}>
<AgoraVideoPlayer
videoTrack={tracks[1]}
style={{ height: "100%", width: "100%" }}
/>
</Grid>
{users.length > 0 &&
users.map((user) => {
if (user.videoTrack) {
return (
<Grid item xs={gridSpacing}>
<AgoraVideoPlayer
videoTrack={user.videoTrack}
key={user.uid}
style={{ height: "100%", width: "100%" }}
/>
</Grid>
);
} else return null;
})}
</Grid>
);
}
Controls.jsx:
import React, { useState } from "react";
import { useClient } from "./settings";
import { Grid, Button } from "#material-ui/core";
import ExitToAppIcon from "#material-ui/icons/ExitToApp";
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faMicrophone, faMicrophoneSlash, faVideoSlash, faVideo } from "#fortawesome/free-solid-svg-icons";
import { useTranslation } from 'react-i18next';
export default function Controls(props) {
const {t} = useTranslation();
const client = useClient();
const { tracks, setStart, setInCall } = props;
const [trackState, setTrackState] = useState({ video: true, audio: true });
const mute = async (type) => {
if (type === "audio") {
await tracks[0].setEnabled(!trackState.audio);
setTrackState((ps) => {
return { ...ps, audio: !ps.audio };
});
} else if (type === "video") {
await tracks[1].setEnabled(!trackState.video);
setTrackState((ps) => {
return { ...ps, video: !ps.video };
});
}
};
const leaveChannel = async () => {
await client.leave();
client.removeAllListeners();
tracks[0].close();
tracks[1].close();
setStart(false);
setInCall(false);
};
return (
<Grid container spacing={2} alignItems="center">
<Grid item>
<Button variant="contained" color={trackState.audio ? "primary" : "secondary"} onClick={() => mute("audio")} >
{trackState.audio ? <FontAwesomeIcon icon={faMicrophone} /> : <FontAwesomeIcon icon={faMicrophoneSlash} />}
</Button>
</Grid>
<Grid item>
<Button variant="contained" color={trackState.video ? "primary" : "secondary"} onClick={() => mute("video")} >
{trackState.video ? <FontAwesomeIcon icon={faVideo} /> : <FontAwesomeIcon icon={faVideoSlash} />}
</Button>
</Grid>
<Grid item>
<Button variant="contained" color="default" onClick={() => leaveChannel()} >
{t('agora.leave')}
<ExitToAppIcon />
</Button>
</Grid>
</Grid>
);
}
settings.js:
import {createClient, createMicrophoneAndCameraTracks} from 'agora-rtc-react';
export const config = {mode: 'rtc', codec: "vp8", appId: process.env.AGORA_APP_ID, token : process.env.AGORA_TOKEN};
export const useClient = createClient(config);
export const useMicrophoneAndCameraTracks = createMicrophoneAndCameraTracks();
export const channelName = process.env.AGORA_CHANNEL_NAME;
AgoraDemo.jsx:
import React, {useState} from "react";
import {Form, Modal, Button,Row, Col} from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import VideoCall from "./agora/VideoCall";
const AgoraDemo = ({subjects}) => {
const {t} = useTranslation();
const [subjectSelected, setSubjectSelected] = useState(null);
const [inCall, setInCall] = useState(false);
return (
<Modal.Dialog size='xl'>
<Modal.Header style={{ display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<Modal.Title>
{t('agora.headline')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="App" style={{ height: "100%" }}>
{subjectSelected && inCall ? (
<VideoCall setInCall={setInCall} />
) : (
<Button onClick={() => setInCall(true)}>
{t('agora.join call')}
</Button>
)}
</div>
</Modal.Body>
<Modal.Footer/>
</Modal.Dialog>
)
}
export default AgoraDemo;
I don't know much plain javascript so I don't understand why this await does not pass first before proceding to the next line of code and how I should fix it. This is the error:
×
Unhandled Rejection (AgoraRTCException): AgoraRTCError INVALID_OPERATION: Can't publish stream, haven't joined yet!
▶ 2 stack frames were collapsed.
async init
C:/Users/User/Desktop/Agora/src/agora/VideoCall.jsx:53
50 |
51 | debugger;
52 |
53 | if (tracks) await client.publish([tracks[0], tracks[1]]);
| ^ 54 | setStart(true);
55 | };
56 |
The implementation was working. The problem was with the token. It turned out it expires (but didn't get afteh how long exactly) and then you get the given error. For me the solution was to obtain a token for every video call. This way I don't need to renew it manually every time it expires :)
I'm using formik to create a reusable component. This is how I've created a container to render a formik form.
I need to infer types from props; since, i've cloned a children, also passed on the props from the container, I need to infer types in the FormFields
import React from 'react';
import { Formik } from 'formik';
import { Container, Grid } from '#mui/material';
import MainCard from 'ui-component/cards/MainCard';
interface ContainerProps<T> {
title: string;
initialValues: T;
validationSchema: Object;
handleSubmit: (values: T, setSubmitting: (isSubmitting: boolean) => void) => void;
children: React.ReactElement;
others?: any;
}
const FormikContainer = <T,>({ title, initialValues, validationSchema, handleSubmit, children, ...others }: ContainerProps<T>) => (
<MainCard title={title}>
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
handleSubmit(values, setSubmitting);
}}
>
{(props) => (
<form onSubmit={props.handleSubmit}>
<Grid container gap={3} maxWidth="500px">
{React.cloneElement(children, { ...props })}
</Grid>
</form>
)}
</Formik>
</MainCard>
);
export default FormikContainer;
I'm not sure on how to infer types in the FormFields for all the props associated with it. How can I define types for otherProps and since otherProps are props through formik;I need to infer formikProps types dynamically as well.
FormFields.tsx
/* eslint-disable consistent-return */
/* eslint jsx-a11y/label-has-associated-control: 0 */
import React, { useState } from 'react';
import {
Grid,
TextField,
FormHelperText,
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio,
MenuItem,
Button,
IconButton,
Typography,
InputLabel
} from '#mui/material';
import { FieldArray, getIn } from 'formik';
import { v4 as uuid } from 'uuid';
import AddIcon from '#mui/icons-material/Add';
import DeleteOutlineIcon from '#mui/icons-material/DeleteOutline';
import { ImgWrapper } from './formik.styles';
import { Label } from 'components/form/Form';
type PropTypes = {
formFields: any;
btnLabel: string;
handleAdd?: any;
handleRemove?: any;
otherProps?: any;
};
const ErrorMessage = ({ errors, touched, name }) => {
// console.log(errors);
return <FormHelperText error>{getIn(touched, name) && getIn(errors, name) && getIn(errors, name)}</FormHelperText>;
};
const FormFields = ({ formFields, btnLabel, ...otherProps }): any => {
const { values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, setFieldValue, handleAdd, handleRemove } =
otherProps;
const [img, setImg] = useState<string>('');
return (
<>
{formFields.map((field, index) => {
switch (field.type) {
case 'fieldArray':
return (
<Grid key={index} item xs={12}>
<Typography
variant={field.mainHeading.variant || 'h4'}
component="h1"
textAlign={field.mainHeading.align}
sx={{ p: 2 }}
>
{field.mainHeading.title}
</Typography>
<FieldArray name={field.name}>
{({ push, remove }) => {
return (
<Grid container gap={3} maxWidth="100%">
{values[field.name].map((eachField, fieldIndex) => {
const IconButtonList = !field.iconButtonDisable && (
<Grid key={`container-${fieldIndex}`} container>
<Grid item xs={10}>
<Typography variant="h4">{`${field.subHeading} ${
fieldIndex + 1
}`}</Typography>
</Grid>
<Grid
item
xs={2}
sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}
>
<IconButton onClick={() => handleAdd(push)}>
<AddIcon />
</IconButton>
<IconButton
onClick={() => {
handleRemove(eachField.id, values, remove);
}}
disabled={values[field.name].length === 1}
>
<DeleteOutlineIcon />
</IconButton>
</Grid>
</Grid>
);
const QuestionList = field.choice.map((eachChoice, choiceIndex) => {
return (
<Grid key={`question-${choiceIndex}`} item xs={12}>
<InputLabel>{eachChoice.label}</InputLabel>
<TextField
fullWidth
placeholder={eachChoice.placeholder}
name={`${field.name}[${fieldIndex}].${eachChoice.name}`}
label={eachChoice.innerLabel}
type={eachChoice.type}
size="medium"
onBlur={handleBlur}
onChange={handleChange}
/>
<ErrorMessage
{...{
errors,
touched,
name: `${field.name}[${fieldIndex}].${eachChoice.name}`
}}
/>
</Grid>
);
});
return [IconButtonList, QuestionList];
})}
</Grid>
);
}}
</FieldArray>
</Grid>
);
}
})}
<Button type="submit" variant="contained" onSubmit={handleSubmit} disabled={isSubmitting}>
{btnLabel || 'Test Button'}
</Button>
</>
);
};
export default FormFields;
It's how I thought of creating a reusable component using formik. Am I doing it right ?
Form.tsx
import FormikContainer from 'components/formik/FormikContainer';
import FormFields from 'components/formik/FormFields';
import * as Yup from 'yup';
import { v4 as uuid } from 'uuid';
import AddPhotoAlternateOutlinedIcon from '#mui/icons-material/AddPhotoAlternateOutlined';
import { formFields } from 'views/userManagement/appUsers/constants/variables';
const initialValues = {
content: [{ id: uuid(), question: '', answer: '' }]
};
const fields = [
{
name: 'content',
type: 'fieldArray',
mainHeading: { align: 'center', title: 'Main heading' },
subHeading: 'Section',
iconButtonDisable: false,
choice: [
{ name: 'question', type: 'text', label: 'Question', placeholder: 'Enter question' },
{ name: 'answer', type: 'text', label: 'Answer', placeholder: 'Enter answer' }
]
}
];
const Form = () => {
const handleSubmit = (values, setSubmitting: (isSubmitting: boolean) => void) => {
console.log(values);
setSubmitting(false);
};
const handleAdd = (push) => {
push({ id: uuid(), question: '', answer: '' });
};
const handleRemove = (id, values, remove) => {
// const target = values.content.findIndex((value) => value.id === id);
// console.log(target);
// remove(target);
console.log(values);
values.content = values.content.filter((value) => value.id !== id);
};
return (
<FormikContainer<typeof initialValues>
title="Formik Reusable components"
initialValues={initialValues}
validationSchema={Yup.object().shape({
content: Yup.array().of(
Yup.object().shape({
question: Yup.string().required('Question is a required field'),
answer: Yup.string().required('Answer is a required field')
})
)
})}
handleSubmit={handleSubmit}
>
<FormFields formFields={fields} btnLabel="Test Button" handleAdd={handleAdd} handleRemove={handleRemove} />
</FormikContainer>
);
};
export default Form;
I have a page with notes for a specific user, the problem is when i'm trying to access the state of 'my notes' i get 'undefined' on console, in postman, everything works when the user login, and I can see the notes.
notes reducer
import {
NOTES_LIST_REQUEST,
NOTES_LIST_SUCCESS,
NOTES_LIST_FAIL,
} from '../types/noteTypes';
export const noteListReducer = (state = { notes: [] }, action) => {
switch (action.type) {
// whenever call the api its going to:
// request the notes
case NOTES_LIST_REQUEST:
return { loading: true };
// if true..
case NOTES_LIST_SUCCESS:
return { loading: false, notes: action.payload };
//if fail..
case NOTES_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
notes action
import {
NOTES_LIST_REQUEST,
NOTES_LIST_SUCCESS,
NOTES_LIST_FAIL
} from '../types/noteTypes';
import axios from 'axios';
export const listNotes = () => async (dispatch, getState) => {
try {
// will set the loading to true
dispatch({ type: NOTES_LIST_REQUEST });
// fetching the user info from the state
const{
userLogin: { userInfo },
} = getState();
//just like sending bearer token from postman to backend
const options = {
headers: {
Authorization: `Bearer ${userInfo.token}`,
},
};
const data = await axios('http://localhost:5000/api/notes', options);
console.log(data);
// const returnDataFromServer = await data.json()
// .then(dataa=>console.log(dataa))
// console.log(returnDataFromServer);
// if request is success dispatch this action and pass the data to the notes state inside the reducer
dispatch({
type: NOTES_LIST_SUCCESS,
payload: data.data
});
} catch (err) {
const message =
err.response && err.response.returnDataFromServer.message
? err.response.returnDataFromServer.message
: err.message;
// if fails fire this action and pass the message
dispatch({
type: NOTES_LIST_FAIL,
payload: message,
});
}
};
notes page
here I'm trying to access the state of 'my notes' but get undefined
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import MainPage from '../../component/MainPage';
import { Badge, Button, Card, Accordion, AccordionCollapse, AccordionButton } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { listNotes } from '../../redux/actions/notesAction';
import Loading from '../../component/Loading';
import ErrorMessage from '../../component/ErrorMessage';
export default function MyNotes() {
//take notes out from our state
const dispatch = useDispatch();
// name to the state
const noteList = useSelector(state => state.noteList);
console.log(noteList);
//destructing what we need from state
// const { loading, error, notes } = noteList;
const deleteHandler = (id) => {
if (window.confirm('Are You Sure?')) {
}
};
useEffect(() => {
dispatch(listNotes())
}, [dispatch])
// render the notes that come from the backend
return (
<MainPage title="welcome back avi vovgen...">
<Link to='createNewNot'>
<Button className="btn btn-info" style={{ marginLeft: 10, marginBottom: 10 }} size="lg" >
Create new Note
</Button>
</Link>
{/*
{error && <ErrorMessage variant="danger">{error}</ErrorMessage>}
{loading && <Loading />} */}
{/* {notes.map((note) => (
<Accordion key={note._id}>
<Card style={{ margin: 10 }}>
<Card.Header style={{ display: "flex" }}>
<span
style={{
color: "black",
textDecoration: "none",
flex: 1,
cursor: "pointer",
alignSelf: "center",
fontSize: 18
}}>
<AccordionButton as={Card.Text} variant="link" >
{note.title}
</AccordionButton>
</span>
<div>
<Button href={`/note/${note._id}`}>Edit</Button>
<Button variant="danger" className="mx-2" onClick={() => deleteHandler(note._id)}>Delete</Button>
</div>
</Card.Header>
<AccordionCollapse eventKey="">
<Card.Body>
<h4>
<Badge className="btn btn-success">
category-{note.category}
</Badge>
</h4>
<blockquote className="blockquote mb-0">
<p>
{note.content}
</p>
<footer className="blockquote-footer">
Created on-date
</footer>
</blockquote>
</Card.Body>
</AccordionCollapse>
</Card>
</Accordion>
)) */}
}
</MainPage >
)
}
I've been having an issue with react not rendering the updated cartItems in the state. The state updates perfectly fine but the items that I delete don't actually get removed from the rendering.
I'm very new to react, redux and state management but I assume nothing's actually wrong with the state itself, rather how/when the items are being iterated to be rendered. It's only when I refresh the page that it renders accurately.
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { Row, Col, ListGroup, Image, Button } from "react-bootstrap";
import Message from "../components/Message";
import { addToCart, removeFromCart } from "../actions/cartActions";
function Basket({ match, location, history }) {
const productId = match.params.id;
const qty = location.search ? Number(location.search.split("?")[1]) : 1;
const specID = location.search ? Number(location.search.split("?")[2]) : 0;
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
useEffect(() => {
if (productId) {
dispatch(addToCart(productId, qty, specID));
}
}, [dispatch, productId, qty, specID]);
const removeFromCartHandler = (cartItem) => {
dispatch(removeFromCart(cartItem));
};
const checkoutHandler = () => {
history.push("/login?redirect=shipping");
};
return (
<Row>
<Col md={8}>
{cartItems.length === 0 ? (
<Message variant="primary">
Your cart is empty{" "}
<Link to="/">
<strong>Go Back</strong>
</Link>
</Message>
) : (
<div>
<Link to="/">
<strong>Go Back</strong>
</Link>
<ListGroup variant="flush">
{cartItems.map((item) => (
<ListGroup.Item key={item.product} className="cart">
<Row>
{/* <Col xs={4} md={2}>
<Image src={item.img} alt={item.name} fluid rounded />
</Col> */}
<Col xs={4} md={3}>
<Link to={`/product/${item.product}`}>{item.name}</Link> (
{item.size})
</Col>
<Col xs={2} md={2}>
£{item.price}
</Col>
<Col xs={6} md={3}>
Qty
</Col>
<Col xs={6} md={1}>
<Button
type="button"
variant="light"
onClick={() => removeFromCartHandler(item)}
>
<i className="fas fa-trash" />
</Button>
</Col>
</Row>
</ListGroup.Item>
))}
</ListGroup>
</div>
)}
</Col>
</Row>
);
}
export default Basket;
The reducer for the cart.
import { CART_ADD_ITEM, CART_REMOVE_ITEM } from "../constants/cartConstants";
export const cartReducer = (
state = { cartItems: [{ specList: [{ price: [] }] }] },
action
) => {
switch (action.type) {
case CART_ADD_ITEM:
const item = action.payload;
const existItem = state.cartItems.find(
(x) => x.product === item.product && x.size === item.size
);
if (existItem) {
return {
...state,
cartItems: state.cartItems.map((x) =>
x.product === existItem.product ? item : x
),
};
} else {
return { ...state, cartItems: [...state.cartItems, item] };
}
case CART_REMOVE_ITEM:
return {
...state,
cartItems: state.cartItems.filter((x) => x !== action.payload),
};
default:
return state;
}
};
import axios from "axios";
import { CART_ADD_ITEM, CART_REMOVE_ITEM } from "../constants/cartConstants";
export const addToCart = (id, qty) => async (dispatch, getState) => {
const { data } = await axios.get(`/api/products/${id}`);
dispatch({
type: CART_ADD_ITEM,
payload: {
product: data._id,
name: data.name,
image: data.image,
price: data.price,
countInStock: data.countInStock,
qty,
},
});
localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
};
export const removeFromCart = (id) => async (dispatch, getState) => {
dispatch({
type: CART_REMOVE_ITEM,
payload: id,
});
localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
};
You are calling () => removeFromCartHandler(item) but at any other point assume the payload is id. You probably want to do () => removeFromCartHandler(item.id).
I'm creating a small application using only React.js, material-ui and firebase. I don't want to use Redux now in order to be familiar more with react.
I create a form which is described by:
User.jsx:
import React, { Component } from 'react'
import Button from '#material-ui/core/Button'
import Grid from '#material-ui/core/Grid';
import { withStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
import Typography from '#material-ui/core/Typography';
import moment from 'moment';
import db from '../db/config';
import InputTextField from './textField';
import RadioGroup from './radioGroup';
import SnackBar from './snackBar';
const styles = (theme) => ({
button: {
margin: theme.spacing.unit,
},
root: {
display: 'flex',
marginTop: theme.spacing.unit * 8,
padding: theme.spacing.unit * 3,
},
item: {
padding: theme.spacing.unit * 2
}
});
class User extends Component {
state = {
birthday: moment().format('YYYY-MM-DD'),
message: '',
name: '',
open: false,
gender: 'male',
};
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
this.setState({ open: false });
};
handleSubmit = (event) => {
event.preventDefault();
const {
birthday,
name,
gender,
} = this.state;
console.log(`birthday: ${birthday} \n` +
`Name: ${name} \n` +
`gender: ${gender} \n`)
const ref = db.ref('users/');
ref.orderByChild('name').on('child_added', (snapshot) => {
const existedName = (snapshot.val().name).toLowerCase().trim();
const newName = name.toLowerCase().trim();
if(existedName === newName){
this.setState({
open: true,
message: 'Name already exists!!',
})
} else {
ref.push({
name: name.trim(),
gender,
birthday
})
.then(() => {
this.setState({
open: true,
message: 'saved successfully!!',
})
return true;
})
.catch((error) => {
this.setState({
open: true,
message: `Error adding baby: ${error}`,
})
return false;
});
}
})
}
render(){
const { classes } = this.props;
const {
birthday,
message,
name,
open,
gender,
} = this.state;
return (
<div className={classes.root}>
<Grid
container
spacing={40}
justify='center'
direction="column"
alignItems="center"
>
{open && (
<SnackBar
handleClose={this.handleClose}
message={message}
open
/>
)}
<Typography
align="center"
gutterBottom
variant="title"
>
Add New User
</Typography>
<form onSubmit={this.handleSubmit} className={classes.form}>
<Grid item className={classes.item} xs={12}>
<InputTextField
label="Name"
handleChange={this.handleChange('name')}
required
value={name}
type="text"
/>
</Grid>
<Grid item className={classes.item}>
<InputTextField
label="Birthday"
handleChange={this.handleChange('birthday')}
required
value={birthday}
type="date"
InputLabelProps={{shrink: true}}
/>
</Grid>
<Grid item className={classes.item}>
<RadioGroup
name="Gender"
handleChange={this.handleChange('gender')}
value={gender}
/>
</Grid>
<Grid item className={classes.item}>
<Grid
container
direction="row"
>
<Grid item>
<Button
variant="contained"
color="primary"
className={classes.button}
>
Cancel
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
className={classes.button}
type='submit'
>
Save
</Button>
</Grid>
</Grid>
</Grid>
</form>
</Grid>
</div>
)
}
}
User.propTypes = {
classes: PropTypes.object.isRequired,
}
export default withStyles(styles)(User);
SnackBar.jsx:
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import Snackbar from '#material-ui/core/Snackbar';
import IconButton from '#material-ui/core/IconButton';
import CloseIcon from '#material-ui/icons/Close';
const styles = theme => ({
close: {
width: theme.spacing.unit * 4,
height: theme.spacing.unit * 4,
},
});
const SimpleSnackbar = (props) => {
const {
classes,
handleClose,
message,
open,
} = props;
return (
<div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={open}
autoHideDuration={6000}
onClose={handleClose}
ContentProps={{
'aria-describedby': 'message-id',
}}
message={<span id="message-id">{message}</span>}
action={[
<Button key="undo" color="secondary" size="small" onClick={handleClose}>
UNDO
</Button>,
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={classes.close}
onClick={handleClose}
>
<CloseIcon />
</IconButton>,
]}
/>
</div>
);
}
SimpleSnackbar.propTypes = {
classes: PropTypes.object.isRequired,
handleClose: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired,
};
export default withStyles(styles)(SimpleSnackbar);
When I enter the different attributes of the form, I got this warning in the console: Firebase Warning
Added to the message too much recursion described by Firebase: too much recursion
I'm beginner to firebase and it's my first app using it.I think that I missed something or I didn't use the suitable function in order to fetch if a given name already exists. Unless it will be saved. I will be grateful if anyone tried to help me to fix the warning and the error displayed on the console.
I tried this solution and it works:
handleSubmit = (event) => {
event.preventDefault();
const {
birthday,
name,
gender,
} = this.state;
const ref = db.ref('users/');
const onChildAdded = (snapshot) => {
if(snapshot.exists()){
this.setState({
open: true,
message: 'Name already exists!!',
})
} else {
ref.push({
name: name.trim(),
gender,
birthday
})
.then(() => {
this.setState({
open: true,
message: 'saved successfully!!',
})
return true;
})
.catch((error) => {
this.setState({
open: true,
message: `Error adding baby: ${error}`,
})
return false;
});
}
}
ref.orderByChild('name').equalTo(name).once('value').then(onChildAdded);
}