I am trying to build message component in react. Currently I just saving entered messages in localStorage but whenever I am pushing a message 2 rows get pushed in localStorage which should not happen. I have checked it but not able to get why this happening.
message comp
import React, { useState, useEffect } from "react";
interface IMessage {
user: string;
text: string;
}
export const Message = () => {
const [messages, setMessages] = useState<IMessage[]>([]);
const [message, setMessage] = useState("");
const [user, setUser] = useState("testuser");
useEffect(() => {
const fetchedMessages = JSON.parse(localStorage.getItem("messages") as any);
if (fetchedMessages) {
console.log("fetchedMessages=>", fetchedMessages);
}
}, [messages]);
const handleFormSubmit = (event: any) => {
event.preventDefault();
setMessages((messages) => {
const newData = [...messages, { text: message, user: user }];
let oldStorage =
JSON.parse(localStorage.getItem("messages") as any) || [];
const oldStorageN = [...oldStorage, { text: message, user: user }];
localStorage.removeItem("messages");
localStorage.setItem("messages", JSON.stringify(oldStorageN));
return newData;
});
setMessage("");
};
return (
<>
<div className="MessageContainer">
{messages.map((message) => {
return (
<div key={message.text + message.user}>
<div>
<strong>{message.user}</strong>
<div>{message.text}</div>
</div>
</div>
);
})}
</div>
<div>
<form onSubmit={handleFormSubmit}>
<div>
<input
type="text"
name="message"
onChange={(e) => {
setMessage(e.target.value);
}}
value={message}
/>
</div>
<div>
<input type="submit" value="send" />
</div>
</form>
</div>
</>
);
};
This is code link
https://stackblitz.com/edit/react-starter-typescript-qowlvq?file=components/Message.tsx
Note: on StackBlitz it's working fine, but on my machine it's adding two entries per message.
Related
I have an app that allows user take and delete notes and I'm trying to implement the edit function. The problem is that I don't want to take the user to a different page, I was the note they click on to change into a form or some kind of editable space (prepopulated with the current content of the note) that they can then save so that it renders again but with the updated values (Google Keep style).
The things that I'm struggling with is how to change just one note since the notes are mapped to components through an array - I've tried using the filter() method and playing with ternary operators, but I either copy the entire array or nothing happens. I came across this question: Google Keep edit functionality, but I'm struggling to understand what's happening and how to adapt it in my code.
So the main question is: how can I change one component without disrupting the other elements in the array or their positions on the page?
Here's my Note component so far:
import React, { useState, useEffect} from "react";
import axios from "axios";
function Note(props) {
const [noteToEdit, setNoteToEdit] = useState({
title: "",
content: "",
category: ''
})
const [isEditNote, setEditNote] = useState(false)
const [idToEdit, setIdToEdit] = useState('')
function deleteNote(id) {
axios.delete(`http://localhost:5000/notes/${id}`)
.then(() => { console.log("Note successfully deleted")
props.setFetch(true)
});
}
function editNote(id, title, content, category){
setEditNote(true)
setNoteToEdit(prevNote => {
return {
title : title,
content : content,
category : category
};
});
console.log("Current note to edit after useState:")
setIdToEdit(id)
console.log(noteToEdit)
}
return (
<div>
{isEditNote && <h1>want to edit: {idToEdit}</h1>}
{!isEditNote &&
<div>
{props.notes.map((noteItem) => {
return (
<div className="note">
<h1>{noteItem.title}</h1>
<p>{noteItem.content}</p>
<button onClick={() => {editNote(noteItem._id, noteItem.title, noteItem.category, noteItem.content)}}>
Edit
</button>
<button onClick={() => {deleteNote(noteItem._id)}}>
Delete
</button>
<p>{noteItem.category}</p>
</div>
);
})}
</div>
}
</div>
)
}
export default Note
and my CreateArea component:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import CreateCategory from "./CreateCategory";
import Note from "./Note";
import axios from "axios"
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
function CreateArea() {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [fetchCategories, setFetchCategories] = useState(true)
const [noteToEdit, setNoteToEdit] = useState({
title: "",
content: "",
category: ''
})
const [ieditNote, setEditNote] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
if(fetchCategories){
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => {
setCategories(json)
setFetchCategories(false)
})
}
}, [fetchCategories])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
function editNote(id, title, content, category){
setNoteToEdit(prevNote => {
return { ...prevNote };
});
console.log("Current note to edit after useState:")
console.log(noteToEdit)
}
return (
<div>
<Header/>
<div className="categories">
<CreateCategory setFetchCategories={setFetchCategories}/>
<button className="all-button" onClick={()=>{setFetch(true)}}>All</button>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFetch={setFetch}/>
</div>
<div className="notes-container">
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<div className="notes-group">
<Note notes={notes} setFetch={setFetch} editNote={editNote} setEditNote={setEditNote}/>
</div>
</div>
<Footer/>
</div>
);
}
export default CreateArea;
Would appreciate any guidance on this, thanks!
I have a form in a page, when the user inputs the name of a new student and clicks submit, I want the content of that component (the form) to be completely replaced by the submitted name. How can I achieve this (Replace the form with the list onsubmit)?
I have read that I can use conditional rendering to toggle components, but it's not really clear to me how i can apply it here.
StudentListResult.Jsx
import React, { useState } from "react";
import StudentForm from "./StudentForm";
import StudentList from "./StudentList";
const StudentListResult = () => {
const [newStudent, setNewStudent] = useState("");
const [students, setStudentsList] = useState([]);
return (
<div>
<div>
<StudentForm
newStudent={newStudent}
setNewStudent={setNewStudent}
students={students}
setStudentsList={setStudentsList}
/>
</div>
<div>
<StudentList students={students} setStudentsList={setStudentsList} />
</div>
</div>
);
};
export default StudentListResult;
StudentListForm
import React from "react";
import { v4 as uuidv4 } from "uuid";
const StudentListForm = ({
newStudent,
setNewStudent,
students,
setStudentsList,
}) => {
const addStudent = (event) => {
event.preventDefault();
setStudentsList([...students, { id: uuidv4(), name: newStudent }]);
setNewStudent("");
};
return (
<form onSubmit={addStudent}>
<div>
<input
value={newStudent}
type="text"
placeholder="Student Name"
onChange={(e) => setNewStudent(e.target.value)}
/>
</div>
<div>
<button>Submit</button>
</div>
</form>
);
};
export default StudentListForm;
StudentList.jsx
import React from "react";
const StudentList = ({ students = [], setStudentsList }) => {
return (
<div>
{students.map((student) => (
<ul key={student.id}>
<li>
<p>{student.name}</p>
</li>
</ul>
))}
</div>
);
};
export default StudentList;
So you want to show the form if not submitted and show the list if submitted? You can add a piece of state called submitted and do simple conditional rendering.
const StudentListResult = () => {
const [submitted, setSubmitted] = useState(false)
return (
{submitted ? <StudentList /> : <StudentListForm />}
);
};
And then in your addStudent function, set submitted.
const addStudent = (event) => {
// ...
setSubmitted(true)
}
If you want change form and list visibility state, you need pass custom function to form component:
StudentListResult.jsx:
const StudentListResult = () => {
const [newStudent, setNewStudent] = useState("");
const [students, setStudentsList] = useState([]);
const [getFormSubmitted, setFormSubmitted] = useState(false);
const setCompletedForm = () => {
setFormSubmitted(!getFormSubmitted);
};
return (
<div>
{getFormSubmitted ? (
<div>
<StudentList students={students} setStudentsList={setStudentsList} />
</div>
) : (
<div>
<StudentForm
newStudent={newStudent}
setNewStudent={setNewStudent}
students={students}
setStudentsList={setStudentsList}
onComplete={setCompletedForm}
/>
</div>
)}
</div>
);
};
Then call this function if form is submitted and all conditions is true
StudentListForm.tsx:
const StudentListForm = ({
newStudent,
setNewStudent,
students,
setStudentsList,
onComplete
}) => {
const addStudent = (event) => {
event.preventDefault();
setStudentsList([...students, { id: uuidv4(), name: newStudent }]);
setNewStudent("");
onComplete();
};
I don't understand why my page can't recognize other pages when I click (for example on page 2, the same page appears again and again)
This is in MealNew.js component:
import React, {useEffect, useState } from "react";
import './MealNew.css';
import Card from "../UI/Card";
import AppPagination from "./AppPagination";
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query,setQuery] = useState('');
const[page,setPage] = useState(9);
const[numberOfPages,setNumberOfPages]= useState(10);
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=991fbfc719c743a5896bebbd98dfe996&page=${page}`;
fetch (link)
.then ((response)=> response.json())
.then ((data) => {
setData(data.results)
setNumberOfPages(data.total_pages)
const elementFood = data?.map((meal,key) => {
return (<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image}
alt='e-meal'/>
</div> )
})
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
}
useEffect(()=> {
handleClick();
},[page])
return (
<Card className="meal">
<form onSubmit={handleSubmit}>
<input
className="search"
placeholder="Search..."
value={query}
onChange={(e)=>setQuery(e.target.value)}/>
<input type='submit' value='Search'/>
</form>
<li className="meal">
<div className = 'meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination
setPage={setPage}
pageNumber={numberOfPages}
/>
</div>
</li>
</Card>
) }
export default MealNew;
This is in AppPagination.js component:
import React from "react";
import { Pagination } from "#mui/material";
const AppPagination = ({setPage,pageNumber}) => {
const handleChange = (page)=> {
setPage(page)
window.scroll(0,0)
console.log (page)
}
return (
<div >
<div >
<Pagination
onChange={(e)=>handleChange(e.target.textContent)}
variant="outlined"
count={pageNumber}/>
</div>
</div>
)
}
export default AppPagination;
Thanks in advance, I would appreciate it a lot
The only error I am getting in Console is this:
Line 64:3: React Hook useEffect has a missing dependency: 'handleClick'. Either include it or remove the dependency array react-hooks/exhaustive-deps
You are not following the spoonacular api.
Your link looks like this:
https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&page=${page}
I checked the spoonacular Search Recipes Api and there's no page parameter you can pass. You have to used number instead of page.
When you receive response from the api, it returns the following keys: offset, number, results and totalResults.
You are storing totalResults as totalNumberOfPages in state which is wrong. MUI Pagination count takes total number of pages not the total number of records. You can calculate the total number of pages by:
Math.ceil(totalRecords / recordsPerPage). Let say you want to display 10 records per page and you have total 105 records.
Total No. of Pages = Math.ceil(105/10)= 11
Also i pass page as prop to AppPagination component to make it as controlled component.
Follow the documentation:
Search Recipes
Pagination API
Complete Code
import { useEffect, useState } from "react";
import { Card, Pagination } from "#mui/material";
const RECORDS_PER_PAGE = 10;
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [numberOfPages, setNumberOfPages] = useState();
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&number=${page}`;
fetch(link)
.then((response) => response.json())
.then((data) => {
setData(data.results);
const totalPages = Math.ceil(data.totalResults / RECORDS_PER_PAGE);
setNumberOfPages(totalPages);
});
};
const elementFood = data?.map((meal, key) => {
return (
<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image} alt='e-meal' />
</div>
);
});
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
};
useEffect(() => {
handleClick();
console.log("first");
}, [page]);
return (
<Card className='meal'>
<form onSubmit={handleSubmit}>
<input className='search' placeholder='Search...' value={query} onChange={(e) => setQuery(e.target.value)} />
<input type='submit' value='Search' />
</form>
<li className='meal'>
<div className='meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination setPage={setPage} pageNumber={numberOfPages} page={page} />
</div>
</li>
</Card>
);
};
const AppPagination = ({ setPage, pageNumber, page }) => {
const handleChange = (page) => {
setPage(page);
window.scroll(0, 0);
console.log(page);
};
console.log("numberOfPages", pageNumber);
return (
<div>
<div>
<Pagination
page={page}
onChange={(e) => handleChange(e.target.textContent)}
variant='outlined'
count={pageNumber}
/>
</div>
</div>
);
};
export default MealNew;
I have a state that contains another array. I need to get this array to return it as a list. The new Item should appear as an object in the application array. I don't quite understand what I did wrong. How can I fix this?
** enter image description here
import React, { useState } from 'react'
function App() {
const [data, setData] = useState([
{
name: 'Ivan Pupkin',
email: 'ivan#gmail.com',
phone: '+34452344323',
application: [
{
nameOfApp: 'Name of App',
type: 'It and business',
description: 'some description',
},
],
},
])
const [name, setName] = useState('');
const [type, setType] = useState('');
const [description, setDescription] = useState('');
const addNewUser = (e) => {
e.preventDefault()
setData(current => current.map(item => [...item.application, {
personalId: 4,
nameOfApp: name,
description: description,
type: type
}]))
}
const Users = data.map(item => item.application.map((elem, index) => {
return(
<div key={index}>
<div>{elem.nameOfApp}</div>
<div>{elem.type}</div>
<div>{elem.description}</div>
</div>
)
}))
return (
<div>
<form action="#">
<input onChange={(e) => setName(e.target.value)} placeholder='name'/>
<input onChange={(e) => setType(e.target.value)} placeholder='type'/>
<input onChange={(e) => setDescription(e.target.value)} placeholder='desc'/>
<button onClick={addNewUser} type='submit'>submit</button>
</form>
<br />
<br />
<br />
{Users}
</div>
)
}
export default App
I'm not sure if I've understand what you're tring to do but here is a working version :
Basically, I just say which user I want to edit in addUser() then search it, assign new application to it, and return the array of users without the previous user
If I were you, I would avoid to have objects / array without ids :)
import React, { useState } from "react";
function App() {
const [data, setData] = useState([
{
id: 1,
name: "Ivan Pupkin",
email: "ivan#gmail.com",
phone: "+34452344323",
application: [
{
id: 1,
nameOfApp: "Name of App",
type: "It and business",
description: "some description"
}
]
}
]);
const [name, setName] = useState("");
const [type, setType] = useState("");
const [description, setDescription] = useState("");
const addNewUser = (e, targetUserId) => {
e.preventDefault();
setData((prevUsers) => {
const editedUser = prevUsers.find(({ id }) => id === targetUserId);
editedUser.application.push({
nameOfApp: name,
type: type,
description: description
});
const newUsers = [
...prevUsers.filter(({ id }) => id !== targetUserId),
editedUser
];
return newUsers;
});
};
const Users = data.map((item) =>
item.application.map((elem, index) => {
return (
<div key={index}>
<div>{elem.nameOfApp}</div>
<div>{elem.type}</div>
<div>{elem.description}</div>
</div>
);
})
);
return (
<div>
<form action="#">
<input onChange={(e) => setName(e.target.value)} placeholder="name" />
<input onChange={(e) => setType(e.target.value)} placeholder="type" />
<input
onChange={(e) => setDescription(e.target.value)}
placeholder="desc"
/>
<button onClick={(e) => addNewUser(e, 1)} type="submit">
submit
</button>
</form>
<br />
<br />
<br />
{Users}
</div>
);
}
export default App;
Hope it helped you !
What the code looks like rendering the button to show the form
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { deleteSong, getSongs, updateSong } from '../../store/song';
import ReactAudioPlayer from 'react-audio-player';
import { useHistory } from 'react-router';
import SongForm from '../AddSongForm';
import EditSongForm from '../EditSongForm';
const SongList = () => {
const [addShowForm, setAddShowForm] = useState(false);
const [editShowForm, setEditShowForm] = useState(false);
const history = useHistory()
const dispatch = useDispatch();
const songsObj = useSelector((state) => state.songState.entries);
const songs = Object.values(songsObj)
const user = useSelector((state) => state.session.user);
const CurrentUserId = user?.id
const remove = (e) => {
dispatch(deleteSong(e.target.id));
}
const addFormCheck = (e) => {
if (addShowForm) setAddShowForm(false)
if (!addShowForm) setAddShowForm(true)
}
const editFormCheck = (e) => {
if (editShowForm) setEditShowForm(false)
if (!editShowForm) setEditShowForm(true)
}
useEffect(() => {
dispatch(getSongs());
}, [dispatch]);
return (
<div>
<div>
<button onClick={addFormCheck}>add a song</button>
{addShowForm ?
<SongForm />
: null}
</div>
<h1>Song List</h1>
<ol>
{songs.map(({ id, songName, songLink, userId }) => (
<div className='songdetails' key={id}>
<p key={id}>songName={songName}</p>
<ReactAudioPlayer
src={songLink}
autoPlay
controls
key={songLink}
/>
{userId === CurrentUserId ?
<>
<div>
<button id={id} onClick={remove}>remove</button>
</div>
<div>
<button id={id} onClick={editFormCheck}>edit</button>
{editShowForm ?
<EditSongForm props={id} />
: null}
</div>
</>
: null}
</div>
))}
</ol>
</div>
);
};
export default SongList;
The actual form
import { useState } from "react";
import { useDispatch } from "react-redux";
import { updateSong } from "../../store/song";
import { useSelector } from "react-redux";
const EditSongForm = ({ props }) => {
console.log(props)
const dispatch = useDispatch();
const [songName, setSongName] = useState("");
const [songLink, setSongLink] = useState("");
const [errors, setErrors] = useState([]);
const reset = () => {
setSongName("");
setSongLink("");
// setAlbumName('');
// setArtistName('')
};
const user = useSelector((state) => state.session.user);
const userId = user?.id
const handleSubmit = async (e) => {
e.preventDefault();
const updatedSongDetails = {
id: props,
songName,
songLink,
userId
};
let updatedSong = await dispatch(updateSong(updatedSongDetails))
.catch(async (res) => {
const data = await res.json()
if (data && data.errors) setErrors(data.errors)
})
reset();
};
return (
<div className="inputBox">
<h1>Update A Song</h1>
<ul>
{errors.map((error, idx) => <li className='errors' key={idx}>{error}</li>)}
</ul>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setSongName(e.target.value)}
value={songName}
placeholder="Song Name"
name="Song Name"
/>
<input
type="text"
onChange={(e) => setSongLink(e.target.value)}
value={songLink}
placeholder="Song Link"
name="Audio File"
/>
<button type="submit">Submit</button>
</form>
</div>
);
};
export default EditSongForm;
Right now when I have a list of songs and click the button for the edit form to appear it applies to the entire list if I have more than one song uploaded. I'm not sure how to make it specific enough so only one form opens at a time.
The solution was to create a component for specific song details and then render that in the .map.
<h1>Song List</h1>
<ol>
{songs.map(({ id, songName, songLink, userId }) => (
<div>
<SpecificSong id={id} songName={songName} songLink={songLink} userId={userId} />
</div>
))}
</ol>