I can update a Note but if I update it it updates the first note which is in myPosts section.
I really don´t understand. If I click on a Note which I create then it goes to the site "/notesEdit" so and I can see there the current forumName and forumDescription but if I change it, it doesn´t change the current selected note instead the first in List.
Here are the Notes which the user sees which he has created :
import React, { useEffect } from "react";
import { Button, Accordion } from "react-bootstrap";
import { Link } from "react-router-dom";
import MainScreen from "../components/MainScreen";
import ReactMarkdown from "react-markdown";
import { useDispatch, useSelector } from "react-redux";
import { deleteNoteAction, listForumUser } from "../../redux/forum/noteActions";
function NotesMe({ history, search }) {
const dispatch = useDispatch();
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const noteList = useSelector((state) => state.noteList);
const { notes } = noteList;
console.log(noteList);
const deleteHandler = (_id) => {
if (window.confirm("Are you sure?")) {
dispatch(deleteNoteAction(_id));
dispatch(listForumUser());
}
};
useEffect(() => {
dispatch(listForumUser());
}, [dispatch]);
return (
<MainScreen title={` ${userInfo.userName}´s Forum..`}>
<Link to="createForum">
<Button style={{ marginLeft: 10, marginBottom: 6 }} size="lg">
Create New Forum
</Button>
</Link>
{notes &&
notes.map((forum) => (
<Accordion key={forum._id} defaultActiveKey="0">
<Accordion.Item style={{ margin: 10 }} key={forum._id}>
<Accordion.Header style={{ display: "flex" }}>
<span
style={{
color: "black",
textDecoration: "none",
flex: 1,
cursor: "pointer",
alignSelf: "center",
fontSize: 18,
}}
>
{forum.forumName}
</span>
<div>
<Link
to={{
pathname: "/notesEdit",
query: { forum },
}}
>
<Button id="EditButton">Edit</Button>
</Link>
<Button
variant="danger"
className="mx-2"
onClick={() => deleteHandler(forum._id)}
>
Delete
</Button>
</div>
</Accordion.Header>
<Accordion.Body>
<blockquote className="blockquote mb-0">
<ReactMarkdown>{forum.forumDescription}</ReactMarkdown>
<footer className="blockquote-footer">
Created on{" "}
<cite title="Source Title">
{/* {forum.published_on && forum.published_on.substring(0, 300)} */}
{/* {forum.user.userName && forum.user.userName} */}
</cite>
</footer>
</blockquote>
</Accordion.Body>
</Accordion.Item>
</Accordion>
))}
</MainScreen>
);
}
export default NotesMe;
and this is the site where the user can change it :
import React, { useState, useEffect } from "react";
import { Form, Button, Row, Col } from "react-bootstrap";
import { useLocation } from "react-router";
import MainScreen from "../components/MainScreen";
import { useDispatch, useSelector } from "react-redux";
import { updateNoteAction } from "../../redux/forum/noteActions";
import "./NotesEdit.css";
const NotesEdit = ({ history }) => {
const [forumName, setforumName] = useState("");
const [forumDescription, setforumDescription] = useState("");
const { query } = useLocation();
const dispatch = useDispatch();
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const noteList = useSelector((state) => state.noteList);
const { notes } = noteList;
useEffect(() => {
if (query) {
setforumName(query.forum.forumName);
setforumDescription(query.forum.forumDescription);
}
}, [query]);
const submitHandler = (e) => {
e.preventDefault();
dispatch(updateNoteAction({ forumName, forumDescription }));
};
return (
<MainScreen title="EDIT MyNote">
<div id="ForumEdit">
<Row className="ForumContainer">
<Col md={6}>
<Form onSubmit={submitHandler}>
<Form.Group controlId="forumName">
<Form.Label>forumName</Form.Label>
<Form.Control
id="forumNameInput"
type="text"
placeholder="Enter forumName"
value={forumName}
onChange={(e) => setforumName(e.target.value)}
></Form.Control>
</Form.Group>
<Form.Group controlId="forumDescription">
<Form.Label>forumDescription</Form.Label>
<Form.Control
id="forumDescriptionInput"
type="text"
placeholder="Enter forumDescription"
value={forumDescription}
onChange={(e) => setforumDescription(e.target.value)}
></Form.Control>
</Form.Group>
<Button
id="EditButton"
type="submit"
varient="primary"
onClick={submitHandler}
>
Update
</Button>
</Form>
</Col>
<Col
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
></Col>
</Row>
</div>
</MainScreen>
);
};
export default NotesEdit;
export const updateNoteAction = (forum) => async (dispatch, getState) => {
try {
dispatch({
type: NOTES_UPDATE_REQUEST,
});
const {
userLogin: { userInfo },
} = getState();
const url = "http://localhost:8080/forum/";
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`,
},
};
const { data } = await axios.put(url, forum, config);
dispatch({
type: NOTES_UPDATE_SUCCESS,
payload: data,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({
type: NOTES_UPDATE_FAIL,
payload: message,
});
}
};
const forumSchema = ({
forumName: {
type: String,
required: true,
},
forumDescription: {
type: String,
required: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'user',
},
published_on: {
type: String,
default: moment().format("LLL")
},
});
Related
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 >
)
}
PROBLEM:
I am currently creating a react app that allows you to checkout a book to a professor. In this app it has a couple of things that need to obviously update when a user checks out a book.
So first off there is the number of totalBooks that is checked out, or just when the entire book object changes then the component should re-render.
I have a useEffect function that is making an api call to a mongodb database and is accessing a document that will yield a book object in the response to the react app. Here is that useEffect function:
useEffect(() => {
const getBook = async () => {
// console.log(book)
await api.getBookById(props.id).then(async book => {
setBook({...book.data.data})
setCopies([...book.data.data.copies])
var num = 0;
await book.data.data.copies.map(async (copy, index) => {
if(copy.checkedOut){
num++;
}
})
setNumCheckedOut(num)
}).catch(e => {console.log(e)})
}
getBook();
}, [book])
I have even subbed out the book object dependency for something like book.checkedOutCopies. Which should return a number and if that number is different from the last then it should re-render the component. This is however, not the case. No matter what I try I am unable to re-render the component when this document changes. I even created a number called reRender and updated it when the api call to checkout a book finished its call. This would be undesired even if it worked because it would not change for someone who was already on the page, but not on the same computer as the person that clicked the checkout button.
I simply just want this component to re-render when the book object in the mongo db database has changed. Which from my understanding the right way to do it is above. The problem is that even after I successfully checkout a book the state never updates. The number of checked out books on the screen stays static:
Here is a screen shot of what the page looks like:
The green books should turn to red when the update button is clicked and a success status is responded. The Total Checked Out should also change. Neither of these happen.
Here is the book object:
const Book = new Schema(
{
bookName: { type: String, required: true },
bookDesc: { type: String, required: false},
numCheckedOut: { type: Number, required: false },
copiesAvail: {type: Number},
whosChecked: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'prof'
}],
copies: [Copy],
ISBN: { type: Number, required: true },
nextDue: {type: String},
nextProf: {type: Schema.Types.ObjectId, ref: 'prof'}
},
{ timestamps: true },
)
I don't understand why it isn't updating would appreciate any help here is the file in its entirety:
import React, { useState, useEffect, useRef } from 'react'
import api from '../api'
import { InputGroup, Form, FormControl, Container, Button, Col, Row, Toast } from 'react-bootstrap'
import { FaBook } from 'react-icons/fa'
import BookIconFunc from '../components/helperComponents/bookIconFunc'
import Select from 'react-dropdown-select';
import './bookslist.css'
import Axios from "axios";
import BookIcoContext from '../context/BookIconContext';
import DatePicker from "react-datepicker";
import ColoredLine from '../components/helperComponents/ColoredLine'
import CheckoutBookHistroy from '../components/CheckoutBookHistory/CheckoutBookHistory'
import { useHistory } from 'react-router-dom';
const handleCheckout = async (e) => {
e.preventDefault();
}
const topLeftBook = {
marginTop: "1.0rem",
display: "flex",
width:"fit-content",
height: "fit-content",
justifyContent: "left",
flexDirection: "column",
alignItems: "center",
borderRadius: "0px",
border: "2px solid #73AD21",
padding: "0.5rem 2.5rem",
}
const booksRows = {
// marginTop: "4.0rem",
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
padding: "1.0rem",
justifyContent: "left",
// border: "2px solid black",
width: "50%",
marginLeft: "1.0rem"
}
const bottomForm = {
flexGrow: "1"
}
const indBook = {
margin: "0.5rem",
color: "green"
}
const updateButtonStyle = {
display: "flex",
width: "100%",
justifyContent:"center"
}
const topOfPage = {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
verticalAlign: "middle"
}
const bookIcon = {
width: "10.0rem",
}
const bottomOfPage = {
display: "flex",
flexDirection: "row",
marginBottom: "1.0rem",
verticalAlign: "middle",
flexGrow: "1",
marginLeft: "1.0rem"
}
const topForm = {
width: "100%",
marginLeft: "2.0rem",
verticalAlign: "middle",
alignItems: "center",
justifyContent: "center"
}
export default function BooksUpdate(props) {
/* TOP FORM VARIABLES */
const [book, setBook] = useState(null)
const [bookName, setBookName] = useState()
const [desc, setDesc] = useState()
const [ISBN, setISBN] = useState()
const [copies, setCopies] = useState()
const history = useHistory();
/* BOOK ICON CONTEXT STATE */
const [iconState, setIconState] = useState(false)
/* re render state when */
const [reRender, setReRender] = useState(0);
/* BOTTOM FORM VARIABLES */
const [newCopies, setNewCopies] = useState()
const [checkoutCopiesNum, setCheckoutCopiesNum] = useState(0)
const [numCheckedOut, setNumCheckedOut] = useState(0)
const [allProfs, setAllProfs] = useState()
const [profChosen, setProfChosen] = useState()
const [bookicoref, setIcoRefs] = useState([]);
const [dueDate, setDueDate] = useState(new Date());
var anotherRender = 0;
const submitCheckout = async (e) => {
e.preventDefault();
try {
const checkoutBookData = {book, checkoutCopiesNum, profChosen, dueDate};
const checkoutBookRes = await Axios.post("http://localhost:8174/api/book/checkout/" + props.id, checkoutBookData)
if(checkoutBookRes.statusText === 'OK'){
setShowCheckoutToast(!showCheckoutToast)
}
/* Display toast that confirms the book was checked out */
setReRender(reRender+1)
anotherRender = anotherRender + 1
// history.push("/Books/Update/" + props.id)
}
catch (err) {
alert(err)
}
}
const handleSetBook = (bookData) => {
setBook({...bookData})
}
useEffect(() => {
const getBook = async () => {
// console.log(book)
await api.getBookById(props.id).then(async book => {
await handleSetBook(book.data.data)
// setBook(book.data.data)
setCopies([...book.data.data.copies])
var num = 0;
await book.data.data.copies.map(async (copy, index) => {
if(copy.checkedOut){
num++;
}
})
setNumCheckedOut(num)
}).catch(e => {console.log(e)})
}
getBook();
}, [book])
useEffect( () => {
const getProfs = async () => {
await Axios.get('http://localhost:8174/user/professors').then((ps) => {
var array = []
ps.data.data.map((prof, index) => {
array.push({label: prof.name, value: prof, key: prof._id})
})
setAllProfs(array)
})
}
getProfs()
}, [])
/* EFFECT TO CREATE REFS FOR EACH BOOK ICON */
useEffect(() => {
// add or remove refs
copies &&
setIcoRefs(bookicorefs => (
Array(copies.length).fill().map((_, i) => bookicorefs[i] || React.createRef())
))
}, [copies]);
const handleUpdate = () => {
console.log("handling update")
}
const findCheckedOut = (array) => {
var numChecked = 0;
array.filter(arr => {
if(arr.checkedOut){
numChecked = numChecked + 1
}
})
return numChecked
}
const [showCheckoutToast, setShowCheckoutToast] = useState(false)
const toggleCheckToast = () => {
setShowCheckoutToast(!showCheckoutToast)
}
/* EFFECT TO VALIDATE THE INFORMATION INSIDE THE CHECKOUT BOOKS FIELD */
useEffect(() => {
if(!copies){
return
}
if(checkoutCopiesNum > copies.length){
alert("There isn't that much of this book available")
return;
}
// console.log(numCheckedOut)
if(checkoutCopiesNum > (copies.length - numCheckedOut)){
setCheckoutCopiesNum(0)
alert('You cannot checkout that many copies as there is already to many checked out')
return;
}
// for(var i = 0; i < checkoutCopiesNum; i++){
// }
},[checkoutCopiesNum, numCheckedOut])
return (
book ?
copies ?
<div>
<Container>
{/* Show the book icon with the title of the book underneath */}
<div style={topOfPage}>
<div style={topLeftBook}>
<FaBook style={bookIcon} size={200}/>
<h4 className="">{book.bookName}</h4>
</div>
<Form style={topForm}>
<Row>
<Col className="pl-0">
<InputGroup className="mb-3 mt-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Book Name</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setBookName(e.target.value)}
defaultValue={book.bookName}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<Form.Group controlId="exampleForm.ControlTextarea1">
<Form.Control as="textarea" rows={5} defaultValue={book.bookDesc}/>
</Form.Group>
</Col>
<Col className="m-0 pr-0">
<InputGroup className="mb-4 mt-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">ISBN</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setISBN(e.target.value)}
defaultValue={book.ISBN}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<InputGroup className="mb-4 mt-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Total Copies</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setNewCopies(e.target.value)}
aria-label="Name"
aria-describedby="basic-addon1"
defaultValue={copies.length}
/>
</InputGroup>
<InputGroup className="mb-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Total Checked Out</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setNewCopies(e.target.value)}
aria-label="Name"
aria-describedby="basic-addon1"
defaultValue={findCheckedOut(book.copies)}
/>
</InputGroup>
</Col>
<Button style={updateButtonStyle} onClick={handleUpdate}>Update</Button>
</Row>
</Form>
</div>
<Row style={{justifyContent: "space-between", verticalAlign: "middle"}}>
<Toast
show={showCheckoutToast}
onClose={toggleCheckToast}
style={{
position: 'absolute',
top: 0,
right: 0,
}}
>
<Toast.Header>
<img
src="holder.js/20x20?text=%20"
className="rounded mr-2"
alt=""
/>
<strong className="mr-auto">Success!</strong>
</Toast.Header>
<Toast.Body>Successfully Checked out a Book</Toast.Body>
</Toast>
<div style={bottomOfPage}>
<Form style={bottomForm} onSubmit={submitCheckout}>
<InputGroup className="mt-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon4">Checkout Out Copies:</InputGroup.Text>
</InputGroup.Prepend>
<FormControl
onChange={(e) => setCheckoutCopiesNum(e.target.value)}
placeholder={checkoutCopiesNum}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<Select
className="mt-4"
style={{width: "100%"}}
name="Select"
required
// loading
searchable
placeholder="To:"
options={allProfs}
onChange={(values) => {setProfChosen(values[0].value)}}
/>
<DatePicker className="mt-4" selected={dueDate} onChange={date => setDueDate(date)} />
<Button type="submit" className="mt-3 w-100">Checkout</Button>
</Form>
</div>
<BookIcoContext.Provider value={{iconState, setIconState}}>
<div style={booksRows} onClick={() => setIconState(true)} onMouseUp={() => setIconState(false)}>
{
copies ? copies.map((copy, index) => {
return <div
key={index}
>
<BookIconFunc
checkedOut={copy.checkedOut}
ref={bookicoref[index]}
>
</BookIconFunc>
</div>
})
:
<div>none</div>
}
</div>
</BookIcoContext.Provider>
</Row>
</Container>
<ColoredLine color="grey" m={20} height={1}/>
<Container>
{/* {book.whosChecked.map(prof => {
// console.log(prof)
// <Col>{prof}</Col>
})} */}
<CheckoutBookHistroy book_id={props.id} book={book} reRender={reRender}></CheckoutBookHistroy>
</Container>
</div>
:
<div>no data</div>
:
<div>no data</div>
)
}
I'm not 100% sure what's going on here. I've got a display component that displays a bunch of cards, using a map based on my database - On the card is an edit button that pops a modal up, passing props over to the edit form.. Here's kinda how it looks:
import React, { useState } from 'react'
import { useQuery, useMutation } from '#apollo/client'
import { GET_ALL_PROJECTS, REMOVE_PROJECT } from '../helpers/queries'
import { makeStyles } from '#material-ui/core/styles'
import DeleteIcon from '#material-ui/icons/Delete'
import EditIcon from '#material-ui/icons/Edit'
import AddForm from './AddForm'
import EditForm from './EditForm'
import AlertMessage from '../Alerts/AlertMessage'
import { Grid, Typography, Card, CardActionArea, CardActions, CardContent, CardMedia, Button, Modal, Backdrop, Fade } from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
const DisplayProjects = () => {
const styles = useStyles()
const [deleteItem] = useMutation(REMOVE_PROJECT)
const { loading, error, data } = useQuery(GET_ALL_PROJECTS)
const [status, setStatusBase] = useState('')
const [resultMessage, setResultMessage] = useState('')
const [addOpen, setAddOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const onDelete = (id, e) => {
e.preventDefault()
deleteItem({
variables: { id },
refetchQueries: [{ query: GET_ALL_PROJECTS }]
}).then(
res => handleSuccess(res),
err => handleError(err)
)
}
// Handles Result of the Delete Operation
const handleSuccess = (res) => {
console.log(res.data.deleteProject.proj_name)
// console.log('success!');
setResultMessage(res.data.deleteProject.proj_name)
setStatusBase({
msg: `Successfully Deleted ${resultMessage}`,
key: Math.random()
})
}
const handleError = (err) => {
console.log('error')
}
//Handles the Modal for Add Project
const handleAddOpen = () => {
setAddOpen(true);
};
const handleAddClose = () => {
setAddOpen(false);
};
//Handles the Modal for Edit Project
const handleEditOpen = () => {
setEditOpen(true);
};
const handleEditClose = () => {
setEditOpen(false);
};
if (loading) return '...Loading'
if (error) return `Error: ${error.message}`
return (
<div>
<div style={{ marginTop: 20, padding: 30 }}>
<Grid container spacing={8} justify='center' alignItems='center'>
{data.projects.map(p => {
return (
<Grid item key={p._id}>
<Card >
<CardActionArea>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CardMedia
style={{ width: 400, height: 100, paddingTop: 10, }}
component='img'
alt='Project Image'
height='140'
image={require('../../images/html-css-javascript-lg.jpg')}
/>
</div>
<CardContent >
<Typography gutterBottom variant='h5' component="h2">
{p.proj_name}
</Typography>
<Typography component='p'>
{p.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button>
<DeleteIcon onClick={e => onDelete(p._id, e)} />
</Button>
<Button onClick={handleEditOpen}>
<Modal
open={editOpen}
onClose={handleEditClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={editOpen}>
<div className={styles.paper}>
<EditForm
id={p._id}
close={handleEditClose}
name={p.proj_name}
desc={p.description}
gh={p.gh_link}
live={p.live_link}
img={p.image_url}
/>
</div>
</Fade>
</Modal>
<EditIcon />
</Button>
</CardActions>
</Card>
{ status ? <AlertMessage key={status.key} message={status.msg} /> : null}
</Grid>
)
}
)}
</Grid>
<Button type='button' onClick={handleAddOpen}>Add Project</Button>
<Modal
open={addOpen}
onClose={handleAddClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={addOpen}>
<div className={styles.paper}>
<AddForm close={handleAddClose} />
</div>
</Fade>
</Modal>
</div>
</div >
)
}
export default DisplayProjects
And here's the form. I've destructured out the props into variables and placed them into a state object called details, so they can be overwritten and submitted to the database..
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { useMutation, useQuery } from '#apollo/client'
import { EDIT_PROJECT, GET_ALL_PROJECTS, GET_PROJECT_BY_ID} from '../helpers/queries'
const AddForm = (props) => {
const params = useParams()
const id = params.toString()
// console.log(id);
const [editProjectItem] = useMutation(EDIT_PROJECT)
const {loading, data, error} = useQuery(GET_PROJECT_BY_ID, {
variables: {
id
},
})
const [details, setDetails] = useState({})
if (loading) return '...Loading';
if (error) return <p>ERROR: {error.message}</p>;
if (!data) return <p>Not found</p>;
setDetails(data.projectById)
console.log(data.projectById)
const submitForm = e => {
e.preventDefault()
try {
editProjectItem({
variables: { id, proj_name, description, gh_link, live_link, image_url},
refetchQueries: [{query: GET_ALL_PROJECTS}]
})
}
catch (err) {
console.log('You Goofed')
}
// setDetails({
// proj_name: '',
// description: '',
// gh_link: '',
// live_link: '',
// image_url: ''
// })
props.close()
}
const changeDetails = (e) => {
setDetails({
...details,
[e.target.name]: e.target.value
})
}
const {_id, proj_name, description, gh_link, live_link, image_url} = details
return (
<div key = {_id}>
<h2>Edit {proj_name}</h2>
<form onSubmit = {submitForm} >
<label>
Project Name:
<input
name = 'proj_name'
value = {proj_name}
onChange = {changeDetails}
/>
</label>
<label>Description</label>
<input
name = 'description'
value = {description}
onChange = {changeDetails}
/>
<label>GitHub Link</label>
<input
name = 'gh_link'
value = {gh_link}
onChange = {changeDetails}
/>
<label>Live Link</label>
<input
name = 'live_link'
value = {live_link}
onChange = {changeDetails}
/>
<label>Preview Image</label>
<input
name = 'image_url'
value = {image_url}
onChange = {changeDetails}
/>
<button type = 'submit'>Submit</button>
</form>
</div>
)
}
export default AddForm
The problem I'm running into, is that when I access the modal, the props are sent from literally EVERY Object, instead of the one, and displays the data for the last record instead of the one I want to edit You can see what happens here (I logged props.id in order to test) https://imgur.com/a/pcEKl89
What did I miss? (Disclaimer: I am still a student, and learning the craft.. be gentle on my code please)
EDIT: I just realized that I didn't indicate that this is the final form of the EditForm component. I haven't added the logic in to make the updates yet, I just wanted to get the data showing properly first.
EDIT2: I made some changes to how the ID is passed over, I was already using React-Router, so I went ahead and made a route to /edit/:id and then using useParams(), I got the ID that way. It seems to be working, however now I'm getting a Too many re-renders message. Updated the AddForm code above to reflect the changes..
I figured out the re-render issue.. it was as simple as dropping the setDetails function into a useEffect Hook:
useEffect(()=> {
if(data){
setDetails(data.projectById)
}
},[data])
I have two files: one for functions (functions.js) and one for SigninScreen, please see the code below. How can I make the signInUser formula refer back to the SigninScreen (and e.g. update useState value of email/password there)?
Right now, it seems as if the signInUser function does not have access to that scope because I get error message stating 'setEmail is not a function'.
functions.js
export async function signInUser(email, password) {
console.log('Attempting to sign in');
credentials = {
email: email.toString(),
password: password.toString()
}
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
};
const response = await fetch('http://localhost:3000/signin', requestOptions)
const data = await response.json()
setEmail('xaxaxaxa')
console.log(data)
}
SigninScreen.js
import React, { useState, useContext } from 'react'
import { View, StyleSheet, Button } from 'react-native'
import { Text, Input } from 'react-native-elements'
import Spacer from '../components/Spacer'
import { signInUser } from '../functions/functions.js'
import UserContext from '../context/appContext.js'
const SigninScreen = ({ navigation }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
// This can be removed later
const user = useContext(UserContext);
console.log(user);
return (
<UserContext.Consumer>
{({userloggerinfo, updateUserLoggerInfo}) => (
<View style={styles.container}>
<Spacer>
<Text h3>Sign in</Text>
</Spacer>
<Input
label="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
/>
<Spacer />
<Input
secureTextEntry
label="Password"
value={password}
onChangeText={setPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<Spacer>
<Button
title="Sign in"
onPress={() => { signInUser(email, password); console.log('hejjjjjj'); }}
/>
</Spacer>
</View>
)}
</UserContext.Consumer>
)
}
SigninScreen.navigationOptions = () => {
return {
header: null
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
marginBottom: 250
}
})
export default SigninScreen
Kind regards
function.js ---- >
export async function signInUser(email, password,setEmail) {
console.log('Attempting to sign in');
let credentials = {
email: email.toString(),
password: password.toString()
}
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
};
const response = await fetch('http://localhost:3000/signin', requestOptions)
const data = await response.json()
setEmail('xaxaxaxa')
console.log(data)
}
SignInScreen.js ----- >
import React, { useState, useContext } from 'react'
import { View, StyleSheet, Button } from 'react-native'
import { Text, Input } from 'react-native-elements'
import Spacer from '../components/Spacer'
import { signInUser } from '../functions/functions.js'
import UserContext from '../context/appContext.js'
const SigninScreen = ({ navigation }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
// This can be removed later
const user = useContext(UserContext);
console.log(user);
return (
<UserContext.Consumer>
{({userloggerinfo, updateUserLoggerInfo}) => (
<View style={styles.container}>
<Spacer>
<Text h3>Sign in</Text>
</Spacer>
<Input
label="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
/>
<Spacer />
<Input
secureTextEntry
label="Password"
value={password}
onChangeText={setPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<Spacer>
<Button
title="Sign in"
onPress={() => { signInUser(email, password,setEmail); console.log('hejjjjjj'); }}
/>
</Spacer>
</View>
)}
</UserContext.Consumer>
)
}
SigninScreen.navigationOptions = () => {
return {
header: null
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
marginBottom: 250
}
})
export default SigninScreen
Try above code it should work now!! :)
Utilizing the return statement did the trick
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
)