React - Rendering a component by triggering an event in another component - reactjs

The parent component connects to a Google Cloud FireStore and saves all data in to cards using setCards hooks.
Next we import two children components in to our parent component:
<UpdateCard card={card} />
<AddCard totalDoclNumbers={totalDoclNumbers} />
PARENT Component - DockList
import React, { useState, useEffect } from 'react';
import { db } from '../firebase';
import UpdateCard from './UpdateCard';
import AddCard from './AddCard';
const DocList = () => {
const [cards, setCards] = useState([]);
const [beginAfter, setBeginAfter] = useState(0);
const [totalDoclNumbers, setTotalDoclNumbers] = useState(0);
useEffect(() => {
const fetchData = async () => {
const data = await db
.collection('FlashCards')
.orderBy('customId')
.startAfter(beginAfter)
.get();
setCards(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchData();
}, [beginAfter]);
return (
<ul className='list'>
{cards.map((card) => (
<li key={card.id} className='list__item' data-id={card.id}>
<UpdateCard card={card} />
</li>
))}
<AddCard totalDoclNumbers={totalDoclNumbers} />
</ul>
);
};
export default DocList;
Inside UpdateCard, we list all data stored in cards using an unordered list:
import React, { useState } from 'react';
import { db } from '../firebase';
const UpdateCard = ({ card }) => {
const [translatedText, setTranslatedText] = useState(card.translatedText);
const [customId, setCustomId] = useState(card.customId);
const onUpdate = async () => {
await db
.collection('FlashCards')
.doc(card.id)
.update({ ...card, customId, originalText, translatedText, imgURL });
};
return (
<>
<input
type='text'
value={customId}
onChange={(e) => {
setCustomId(Number(e.target.value));
}}
/>
<textarea
value={translatedText}
onChange={(e) => {
setTranslatedText(e.target.value);
}}
/>
<button onClick={onUpdate}>
Update
</button>
</>
);
};
export default UpdateCard;
Finally in the second child component, called AddCard, we have a button, which triggers the function onAdd to add new data in to our FireStore collection.
import React, { useState } from 'react';
import { db } from '../firebase';
const AddCard = ({ totalDoclNumbers }) => {
const [newTranslatedText, setNewTranslatedText] = useState([]);
const nextNumber = totalDoclNumbers + 1;
const onAdd = async () => {
await db.collection('FlashCards').add({
translatedText: newTranslatedText,
customId: Number(nextNumber),
});
};
return (
<ul className='list'>
<li key={nextNumber}>
<input
type='text'
className='list__input'
defaultValue={nextNumber}
/>
<textarea
onChange={(e) => setNewTranslatedText(e.target.value)}
/>
<button onClick={onAdd}>
Add
</button>
</li>
</ul>
);
};
export default AddCard;
It all works. When you click the button inside the second child component AddCard component, the new data get stored in to the collection.
But to be able to see new added data, I need to render UpdateCard and that's exactly, what I'm struggling with.
How can I achieve that click on the button inside the AddCard component, triggers rendering in UpdateCard component.

Ok, so first on DocList add a callback function:
const DocList = () => {
...
const [addButtonClickCount, setAddButtonClickCount] = useState(0);
...
return (
<ul className='list'>
{cards.map((card) => (
<li key={card.id} className='list__item' data-id={card.id}>
<UpdateCard card={card} addButtonClickCount={addButtonClickCount}/>
</li>
))}
<AddCard totalDoclNumbers={totalDoclNumbers} onAddButtonClick={(card) => {
setAddButtonClickCount(c => c + 1)
setCards(cards => [...cards, {...card.data(), id: card.idcard}])
}} />
</ul>
);
};
then call onAddButtonClick which is passed to AddCard as props when needed:
const AddCard = ({ totalDoclNumbers, onAddButtonClick }) => {
...
const onAdd = async () => {
// Somehow you gotta get value of newly created card:
let card = await db.collection('FlashCards').add({
translatedText: newTranslatedText,
customId: Number(nextNumber),
});
// pass the newly created card here so you could use it in `UpdateCard`
onAddButtonClick(card) // this looks likes where it belongs.
};
this will result in rerendering of UpdateCard component since it's getting addButtonClickCount as props, if you want to do something in UpdateCard after add button is clicked, you could use useEffect with [addButtonClickCount] dependency array.

Related

Delete currently clicked item in react

Thanks for the help in advance. Currently, I am learning react. As a part of it I am coding a project with a basic listing and deleting the currently clicked listed item. I was able to list the entered item but was not able to delete it. I was able to fetch the id of the currently clicked item but doesn't have a picture of what to do next.Can anyone please help me to solve this.
My code:
App.js
import React, { useState, Fragment } from "react";
import UserAddForm from "./UserAddForm/UserAddForm";
import UserList from "./UserList/UserList";
const App = () => {
const [dataList, setDataList] = useState([]);
const FormDatas = (datas) => {
setDataList([datas, ...dataList]);
};
const deleteListItem = (clickedListId) => {
console.log(clickedListId);
};
return (
<Fragment>
<UserAddForm enteredFormVals={FormDatas} />
<UserList listDatas={dataList} deleteItem={deleteListItem} />
</Fragment>
);
};
export default App;
UserList.js
import React from "react";
import userListing from "./UserList.module.css";
const UserList = (props) => {
const deleteHandler = (e) => {
console.log(e.target.id);
props.deleteItem(e.target.id);
};
return (
<div className={`${userListing.users} ${userListing.whiteBg}`}>
<ul>
{props.listDatas.map((data) => (
<li key={data.id} id={data.id} onClick={deleteHandler}>
{data.name}
{data.age}
</li>
))}
</ul>
</div>
);
};
export default UserList;
UserAddForm.js
import React, { useState } from "react";
import UserForm from "./UserAddForm.module.css";
const UserAddForm = (props) => {
const [enteredName, setEnteredName] = useState("");
const [enteredAge, setEnteredAge] = useState("");
const nameHandler = (e) => {
setEnteredName(e.target.value);
};
const ageHandler = (e) => {
setEnteredAge(e.target.value);
};
const userFormHandler = (e) => {
e.preventDefault();
const EnteredFormDatas = {
name: enteredName,
age: enteredAge,
id: Math.random().toString(),
};
props.enteredFormVals(EnteredFormDatas);
};
return (
<div className={`${UserForm.input} ${UserForm.whiteBg}`}>
<form onSubmit={userFormHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
onChange={nameHandler}
value={enteredName}
/>
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
onChange={ageHandler}
value={enteredAge}
/>
<button type="submit" className={UserForm.button}>
Add User
</button>
</form>
</div>
);
};
export default UserAddForm;
You need to filter out the item from the array by keeping ones with a different id and set it back as a new dataList.
const deleteListItem = (clickedListId) => {
setDataList(items => items.filter(({ id }) => id !== clickedListId))
};

How to scroll to top at each page change on Pagination component?

I would like to have an automatic scroll up when I change pages thanks to my Pagination component. It works great but I would like to add this feature. I have no idea how to do this..I tried with a tutorial that uses window but it doesn't worked because I've got no redirect, just a component divided into several pages (EventLists)...
Thanks!! Here is my code :
PAGINATION COMPONENT
import PropTypes from 'prop-types';
// import { LinkContainer } from 'react-router-bootstrap';
import './pagination.scss';
const Pagination = ({ postsPerPage, totalPosts, paginate }) => {
const pageNumbers = [];
// eslint-disable-next-line no-plusplus
for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {
pageNumbers.push(i);
}
return (
<nav expand="lg" id="pagination-navbar">
<ul className="pagination">
{pageNumbers.map((number) => (
<li key={number} className="page-item">
<a
style={{ cursor: 'pointer' }}
onClick={() => paginate(number)}
className="page-link"
>{number}
</a>
</li>
))}
</ul>
</nav>
);
};
Pagination.propTypes = {
postsPerPage: PropTypes.number.isRequired,
totalPosts: PropTypes.number.isRequired,
paginate: PropTypes.func.isRequired,
};
export default Pagination;
FILE THAT USES Pagination
import { useState } from 'react';
import { useSelector } from 'react-redux';
// import react-Bootstrap's component(s)
import {
Row,
} from 'react-bootstrap';
// import { useLocation } from 'react-router-dom';
import SearchBar from 'src/components/SearchBar';
import Pagination from 'src/components/Pagination';
import EventCard from '../EventCard';
import './eventsList.scss';
const EventsList = () => {
// TODO code to retrieve the id with a useLocation (not found yet)
// we use useLocation to retrieve the state of the route
// in which we have stored genreId or regionId
// if location is defined, take me its state
// if the state is defined take me the region
// console.log(location.state); => returns null
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage] = useState(9);
const { eventsList } = useSelector((state) => state.events);
// Get current posts
const indexOfLastPost = currentPage * postsPerPage;
const indexofFirstPost = indexOfLastPost - postsPerPage;
const currentEvents = eventsList.slice(indexofFirstPost, indexOfLastPost);
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
return (
<div>
<SearchBar
// we pass a string to change the title according to the page
// we pass the length of the table to boost the results in the title
results={eventsList.length}
message="results"
// genreId={genreId}
// regionId={regionId}
/>
<Row>
{currentEvents.map((item) => (
<EventCard key={item.id} {...item} />
))}
</Row>
<Pagination
postsPerPage={postsPerPage}
totalPosts={eventsList.length}
paginate={paginate}
/>
</div>
);
};
export default EventsList;
You could make use of React Refs: https://reactjs.org/docs/refs-and-the-dom.html.
You create a ref, attach it to the element you want to scroll to, and scroll to that element when the page changes.
Something like:
const EventsList = () => {
const pageTopRef = useRef(null);
const paginate = (pageNumber) => {
setCurrentPage(pageNumber);
pageTopRef.current.scrollIntoView();
};
return (
<div>
...
<Row ref={pageTopRef}>
{currentEvents.map((item) => (
<EventCard key={item.id} {...item} />
))}
</Row>
...
</div>
);
};
This seem to work for me. MUI v5, React
...
const [page, setPage] = useState(props.props.pagination.page);
...
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth',
});
}, [page]);
...
const handleChange = (event, value) => {
setIsLoading(true);
setPage(value);
};
...
<Pagination
...
count={pageCount}
page={page}
onChange={handleChange}
...
/>

Set a default content from database in react-draft-wysiwyg editor

I am creating a blog website in which I am embedding react-draft-wysiwyg editor. I am facing problem when the user has to update the blog. When I click the update button the content is gone. I looked into many solutions but I couldn't make it work.
This is my code
import axios from "axios";
import React, { useContext, useEffect, useState } from "react";
import { useLocation } from "react-router";
import { Link } from "react-router-dom";
import { Context } from "../../context/Context";
import "./singlePost.css";
import { EditorState, ContentState, convertFromHTML } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import { convertToHTML } from 'draft-convert';
import DOMPurify from 'dompurify';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import Parser from 'html-react-parser';
export default function SinglePost() {
const location = useLocation();
const path = location.pathname.split("/")[2];
const [post, setPost] = useState({});
const PF = "http://localhost:5000/images/";
const { user } = useContext(Context);
const [title, setTitle] = useState("");
const [desc, setDesc] = useState("");
const [updateMode, setUpdateMode] = useState(false);
useEffect(() => {
const getPost = async () => {
const res = await axios.get("/posts/" + path);
setPost(res.data);
setTitle(res.data.title);
setDesc(res.data.desc);
};
getPost();
}, [path]);
const handleDelete = async () => {
try {
await axios.delete(`/posts/${post._id}`, {
data: { username: user.username },
});
window.location.replace("/");
} catch (err) {}
};
// updating post
const handleUpdate = async () => {
try {
await axios.put(`/posts/${post._id}`, {
username: user.username,
title,
desc,
});
setUpdateMode(false)
} catch (err) {}
};
const [editorState, setEditorState] = useState(
() => EditorState.createWithContent(
ContentState.createFromBlockArray(
convertFromHTML(desc)
)
),
);
const [convertedContent, setConvertedContent] = useState(null);
const handleEditorChange = (state) => {
setEditorState(state);
convertContentToHTML();
}
const convertContentToHTML = () => {
let currentContentAsHTML = convertToHTML(editorState.getCurrentContent());
setConvertedContent(currentContentAsHTML);
setDesc(currentContentAsHTML);
}
const createMarkup = (html) => {
return {
__html: DOMPurify.sanitize(html)
}
}
return (
<div className="singlePost">
<div className="singlePostWrapper">
{post.photo && (
<img src={PF + post.photo} alt="" className="singlePostImg" />
)}
{updateMode ? (
<input
type="text"
value={title}
className="singlePostTitleInput"
autoFocus
onChange={(e) => setTitle(e.target.value)}
/>
) : (
<h1 className="singlePostTitle">
{title}
{post.username === user?.username && (
<div className="singlePostEdit">
<i
className="singlePostIcon far fa-edit"
onClick={() => setUpdateMode(true)}
></i>
<i
className="singlePostIcon far fa-trash-alt"
onClick={handleDelete}
></i>
</div>
)}
</h1>
)}
<div className="singlePostInfo">
<span className="singlePostAuthor">
Author:
<Link to={`/?user=${post.username}`} className="link">
<b> {post.username}</b>
</Link>
</span>
<span className="singlePostDate">
{new Date(post.createdAt).toDateString()}
</span>
</div>
{updateMode ? (
// <textarea
// className="singlePostDescInput"
// value={desc}
// onChange={(e) => setDesc(e.target.value)}
// />
<Editor
contentState={desc}
editorState={editorState}
onEditorStateChange={handleEditorChange}
wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
/>
) : (
<p className="singlePostDesc">{Parser(desc)}</p>
)}
{updateMode && (
<button className="singlePostButton" onClick={handleUpdate}>
Update
</button>
)}
</div>
</div>
);
}
I want to display desc which is saved in MongoDB database when the user clicks on update button.
The following part is what I tried to do but didn't work.
const [editorState, setEditorState] = useState(
() => EditorState.createWithContent(
ContentState.createFromBlockArray(
convertFromHTML(desc)
)
),
);
I am getting warning in this:
react.development.js:220 Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the r component.
Please help

Delete an item on click

I am trying to write the very first to-do application in REACT. I want to add functionality to delete to-do item when the user clicks on the delete icon. When I click on delete icon it only removes the text. Here I would like to delete the entire item. Can someone please suggest?
App.js
import './App.css';
import { useState } from 'react';
import TodoList from './TodoList';
import { v4 as uuidv4 } from 'uuid';
function App() {
const [input, setInput] = useState('');
const [todos, setTodo] = useState([]);
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([...todos, { id: id, text: input}])
// setTodo({todos: [...todos, input], id })
setInput('');
}
const deleteTodo = (id) => {
console.log("id" + id);
const filteredItem = todos.filter(todo => todo.id !== id);
setTodo([filteredItem]);
}
return (
<div className="App">
<form>
<input type="text" value={input} onChange={(e) => setInput(e.target.value)}/>
<button type="submit" onClick={addTodo}>Enter</button>
</form>
<TodoList todos={todos} deletetodo={deleteTodo}/>
</div>
);
}
export default App;
TodoList.js
import React from 'react'
import DeleteIcon from '#material-ui/icons/Delete';
import EditIcon from '#material-ui/icons/Edit';
const todo = ({todos, deletetodo}) => {
return (
<div>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<div>
<DeleteIcon onClick={(todo) => deletetodo(todo.id)}/>
<EditIcon/>
</div>
</li>
))}
</div>
)
}
export default todo;
There are a few problems with your code. I will start with the most obvious. You re-render your App on EVERY change of the input field. That's just unnecessary. So insated of storing the value of the input in a state variable, I would use useRef(). So you only really need one state variable, one that stores the list of todos.
Second, your filter is correct, but then you incorrectly set the state variable with the filtered result:
const filteredItem = todos.filter(todo => todo.id !== id);
setTodo([filteredItem]);
It will already return an array and there is no need to wrap it into another one.
With those 2 main issue fixed, here is a working example along with a Sandbox:
import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import "./styles.css";
const TodoList = ({ todos, deletetodo }) => {
return (
<div>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<div>
<button onClick={() => deletetodo(todo.id)}>delete</button>
<button>edit</button>
</div>
</li>
))}
</div>
);
};
export default function App() {
const [todos, setTodo] = useState([]);
const input = React.useRef();
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([...todos, { id: id, text: input.current.value }]);
input.current.value = "";
};
const deleteTodo = (id) => {
setTodo(todos.filter((item) => item.id !== id));
};
return (
<div className="App">
<form>
<input ref={input} type="text" />
<button type="submit" onClick={addTodo}>
Enter
</button>
</form>
<TodoList todos={todos} deletetodo={deleteTodo} />
</div>
);
}
You have a mistake in how you're setting todo in deleteTodo:
const deleteTodo = (id) => {
console.log("id" + id);
const filteredItem = todos.filter(todo => todo.id !== id);
// Mistake! Your filteredItem is an array, you're putting your array into an array.
setTodo([filteredItem]);
}
Consequently, when you pass it further down, your component tries to get [filteredItem].text, which is undefined and React sees an empty string.
Fix:
setTodo(filteredItem);
There are multiple issues within the code:
First one is setting the values after deleting the row:
should be like this : setTodo(filteredItem);
Second issue was calling the onClick function, you already have the id with you so no need to re-call it again:
<div>
{todos.map(todoss =>
<li onClick={() => deletetodo(todoss.id)} key={todoss.id}>
{todoss.text}
</li>
)}
</div>

Access useEffect in Parent from Child Component

I'm using react and trying to figure out how to trigger useEffect (on parent) from a child component.
I have a child component that contains a modal with input fields that are passed to my DB.
import React, { useState } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
const AddContact = () => {
// Modal Setup
const [ showModal, setShowModal ] = useState(false);
const handleClose = () => setShowModal(false);
const handleShow = () => setShowModal(true);
// Using state to hold the values for each field in the form
const [first, setFirst] = useState('');
const [last, setLast] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // prevents form default action
const addContact = async () => {
const result = await fetch('/api/crm/add', {
method: 'POST',
body: JSON.stringify({ first_name: first, last_name: last }),
headers: {
'Content-Type': 'application/json',
}
});
const body = await result.json();
}
addContact();
handleClose();
};
return (
<>
<Button onClick={handleShow} variant="primary" size="sm">Add Contact</Button>
<Modal show={showModal} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Add Contact</Modal.Title>
</Modal.Header>
<Modal.Body>
<label for="formfirst">First Name: </label><br />
<input
id="formfirst"
name="formfirst"
type="text"
value={first}
onChange={e => setFirst(e.target.value)}
/>
<br/>
<label for="formlast">Last Name: </label><br />
<input
id="last"
name="last"
type="text"
value={last}
onChange={e => setLast(e.target.value)}
/> <br/>
<Form.Group>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>Cancel</Button>
<Button variant="primary" onClick={handleSubmit}>Submit</Button>
</Modal.Footer>
</Modal>
</>
);
}
export default AddContact;
The parent component has a table of data which I would like to refresh so that it shows the current data:
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Table } from 'react-bootstrap';
import AddContactForm from '../components/AddContactForm';
// React-Table
import {
useTable,
} from 'react-table'
const ContactsPage = () => {
const [ tableData, setTableData ] = useState([]);
useEffect( () => {
const fetchData = async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
}
fetchData();
},[]);
const columns = React.useMemo(
() => [
{
Header: 'First Name',
accessor: 'first_name',
},
{
Header: 'Last Name',
accessor: 'last_name',
}
... SIMPLIFIED FOR CONCISENESS
],
[]
)
function ReactTable({ columns, data }) {
const {
getTableProps,
... REMOVED SOME REACT-TABLE FOR CONCISENESS
} = useTable({
columns,
data,
})
return (
<>
<h2>Contacts</h2>
<hr></hr>
<div>
<AddContactForm />
</div>
<Table striped bordered hover size="sm" {...getTableProps()}>
... REMOVED TABLE TO CONCISENESS
</Table>
</>
);
}
const data = React.useMemo(() => tableData)
return (
<Styles>
<ReactTable columns={columns} data={data} />
</Styles>
)
}
export default ContactsPage;
How can I achieve this? I tried making my useEffect hook into a function which I could pass to my child component, but I got an error saying that that new function was not a function???
Not sure it makes sense to pass useEffect as props, instead pass the function used inside useEffect as props. Here
useEffect( () => {
const fetchData = async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
}
fetchData();
},[]);
Refactor it like this;
// Wrapped in useCallback otherwise it would be recreated each time
// this component rerenders, hence triggering useEffect below
let fetchData = React.useCallback(async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
},[])
useEffect(() => {
fetchData();
},[fetchData]);
and pass fetchData as props to the modal which you can invoke after submit.

Resources