React Form loses input on reload - javascript

im having trouble with a project of mine with the useState hook. This is for a declaration of an event.
This component can be opened from a list of all the other declas{ which works fine} and then everything shows however if you reload the page/open it from a link nothing shows up in the following fields even when it does get the decla using using the useEffect hoop(at least it logs it out to the console)
TLDR;
when component called/opened from a list it works and loads all the data but when the page is reloaded it is all lost and doesnt load it again with useState
ive taken the liberty to reduces the amount of data show as it is a pretty long file already.
import { useContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import Select from "react-select";
import CreatableSelect from "react-select/creatable";
import DeclaContext from "../../context/DeclaContext";
import LedenContext from "../../context/LedenContext";
import EventContext from "../../context/EventContext";
export const DeclaFrom = () => {
let { id } = useParams();
const navigate = useNavigate();
const { user, leden } = useContext(LedenContext);
const { events } = useContext(EventContext);
let { declas, GET, GET_decla, POST, PUT, DELETE, boekstuks, POST_boekstuk } =
useContext(DeclaContext);
// useEffect(() => {
// const get = async () => {
// await GET();
// const data = await GET_decla(id);
// setDecla(data);
// };
// get();
// // eslint-disable-next-line
// }, [id]);
console.log(declas?.find((de) => (de.id === id ? true : false)));
const [decla, setDecla] = useState(declas?.find((de) => de.id === id));
const [event, setEvent] = useState(decla && decla?.event);
const [owner, setOwner] = useState(decla ? decla?.owner : user.lid_id);
const [content, setContent] = useState(decla ? decla?.content : "");
const [total, setTotal] = useState(decla ? decla?.total : 0);
const [receipt, setReceipt] = useState(decla && decla?.receipt);
const [boekstuk, setBoekstuk] = useState(decla ? decla?.boekstuk : "");
const [content_ficus, setContent_ficus] = useState(
decla ? decla?.content_ficus : ""
);
const optionsLeden = [
{ label: "Select All", value: "all" },
...leden?.map((lid) => ({
value: lid.id,
label: lid?.initials,
})),
];
const [defaultValues, setDefaultValues] = useState(
decla
? decla?.present?.map((pres) =>
optionsLeden?.find((x) => x.value === pres)
)
: []
);
const optionsBoekstuk = boekstuks?.map((boekstuk) => ({
value: boekstuk?.id,
label: boekstuk?.name,
}));
const optionsEvents = events?.map((event) => ({
value: event.id,
label: event.description + " " + event.start_date,
}));
async function createBookstuk(inputValue) {
const BSid = await POST_boekstuk({ name: inputValue });
setBoekstuk(optionsBoekstuk?.find((x) => x.id === BSid)?.value);
}
const onDelete = (e) => {
e.preventDefault();
DELETE({
id,
});
setDeleted(true);
};
const onSubmit = (e) => {
// if (!event | !content | !total | !present) {
// alert("Je moet een evenement kiezen");
// return;
// }
e.preventDefault();
if (decla) {
PUT({
id,
event,
content,
total,
receipt,
boekstuk,
});
navigate("/declas");
} else {
POST({
event,
content,
total,
receipt,
boekstuk,
});
}
setDeleted(false);
};
return (
<div className="columns">
<div className="column is-half is-offset-3">
<form>
<table>
<tbody>
{user.roles.includes("Fiscus") && (
<tr>
<th>
<label htmlFor="id_lid">Lid:</label>
</th>
<td className="field">
<Select
defaultValue={optionsLeden?.find(
(x) => x.value === owner
)}
options={optionsLeden?.filter(
(x) => !["all", 19900].includes(x.value)
)}
name="owner"
id="id_present"
onChange={(e) => {
setOwner(e.value);
}}
/>
</td>
</tr>
)}
<tr>
<th>
<label htmlFor="id_event">Event:</label>
</th>
<td className="field">
<Select
defaultValue={optionsEvents?.find(
(x) => x.value === event?.id
)}
options={optionsEvents}
name="event"
onChange={(e) => {
setEvent(e.value);
}}
/>
</td>
</tr>
<tr>
<th>
<label htmlFor="id_total">Total:</label>
</th>
<td className="field">
<input
type="number"
onChange={(e) => setTotal(e.target.value)}
name="total"
value={total}
step="any"
className="input"
required
id="id_total"
/>
</td>
</tr>
<tr>
<th>
<label htmlFor="id_present">Present:</label>
</th>
<Select
isMulti
value={defaultValues}
options={optionsLeden}
required
name="present"
id="id_present"
onChange={(e) => {
if (e.some((val) => val.value === "all")) {
setPresent(
optionsLeden
?.filter((x) => x.value !== "all")
.map((x) => x.value !== "all" && x.value)
); // change the value going to the API
setDefaultValues(
optionsLeden
?.filter((x) => x.value !== "all")
.map((x) => x.value !== "all" && x)
); // change the values displayed
} else {
setPresent(e?.map((x) => x.value)); // change the value going to the API
setDefaultValues(e?.map((x) => x)); // change the values displayed
}
}}
/>
</tr>
<tr>
<th>
<label htmlFor="id_receipt">Receipt:</label>
</th>
<td className="field">
<input
type="file"
name="receipt"
required
accept="image/*"
onChange={(e) => {
setReceipt(e.target.files[0]);
}}
className="input"
id="id_receipt"
/>
</td>
</tr>
<tr>
<th>
<label htmlFor="id_boekstuk">Boekstuk:</label>
</th>
<td className="field">
<CreatableSelect
defaultValue={optionsBoekstuk?.find(
(x) => x.value === boekstuk
)}
options={optionsBoekstuk}
name="boekstuk"
id="id_boekstuk"
onCreateOption={createBookstuk}
onChange={(e) => {
setBoekstuk(e.value);
}}
/>
</td>
</tr>
<tr>
<th>
<label htmlFor="id_content_ficus">Content ficus:</label>
</th>
<td className="field">
<textarea
onChange={(e) => setContent_ficus(e.target.value)}
name="content_ficus"
value={content_ficus}
cols="40"
rows="10"
maxLength="100"
className="input"
id="id_content_ficus"
></textarea>
</td>
</tr>
</tbody>
</table>
</form>
</div>
</div>
);
};
export default DeclaFrom;
Thanks for your help

I had a similar issue in a recent project. The problem you're facing is that your state is being erased when you reload the page.
In your useEffect hook, remove id from the dependency array (but leave the empty array) so that it will run once on every render. This way when you reload the page useEffect will run your api call and re-populate your state.
useEffect(() => {
const get = async () => {
await GET();
const data = await GET_decla(id);
setDecla(data);
};
get();
}, []);
veel geluk!

useState values will not persist after page reloads: this is a standard behavior of React. After page reload, all your useState values will reset to their default values.
If you need values to persist after refresh, then you will want to use a browser utility like localStorage.
As an example, this is how I would set/get the value:
const stateFromLocalStorage = localStorage.getItem("itemKey")
const [someState, setSomeState] = useState(stateFromLocalStorage || defaultState)
const settingState = (key, value) => {
localStorage.setItem(key, value)
setSomeState(value)
}
I grab the item from localStorage first, and attempt to assign it as the default value of the useState. If the local storage value doesn't exist, then stateFromLocalStorage will be undefined, and it will set defaultState instead because of the logical || operator.
When setting the state, I set it both with the useState setter function, as well as the local storage setItem function. That way local storage and component state will stay in sync.

Related

Passing user input from child functional component to parent functional component

Im creating an invoice generator where the user can add an item, its price, and the quantity. I want to access the user inputs as a state from a child functional component (TableItems.js) into a parent functional component (TableSheet.js) to be able to save the user inputs into a database preferably firestore. I'm having a problem accessing the user input value from the child component to the parent component. I have been struggling with this bug for days, i really hope you guys could help me.
This is the Child component
import React, {useState, useEffect} from 'react'
function TableItems({index, tableItem }) {
const [price, setPrice] = useState(0);
const [qty, setQty] = useState(0);
const [total, setTotal] = useState([]);
useEffect(() => {
//arithmetically add price and qty values
const x = Number(price) * Number(qty)
setTotal(x)
return () => {
//clean up function will be here
};
}, [price, qty, total ]);
return (
<>
<tr>
<td><input type='text' required/></td>
<td><input type='number' value={price} onChange={(e) => setPrice(e.target.value)}/></td>
<td><input type='number' value={qty} onChange={(e) => setQty(e.target.value)}/></td>
<td>{total}</td>
</tr>
</>
)
}
export default TableItems
This is the Parent component
import React, { useState } from 'react'
import TableItems from './TableItems'
function TableSheet() {
const [tableItem, setTableItem] = useState([1]);
//adding a new table cell (table row)
const addCell = () => {
setTableItem((t) => [...t, t + 1])
}
return (
<div>
<table>
<thead>
<th>Item Description</th>
<th>Price</th>
<th>Qty.</th>
<th>Total</th>
</thead>
{
tableItem.map((tableItem, index, setItem) => {
return <TableItems key={index} tableItem={tableItem} setItem={setItem} addCell={addCell}/>
})
}
</table>
<button onClick={addCell}>+</button>
</div>
)
}
export default TableSheet
You tableItem state should contains item objects (quantity and price)
TableItems
function TableItems({ index, tableItem, onChangeItem }) {
return (
<>
<tr>
<td>
<input type="text" required />
</td>
<td>
<input
type="number"
value={tableItem.price}
onChange={(e) => onChangeItem(index, "price", e.target.value)}
/>
</td>
<td>
<input
type="number"
value={tableItem.quantity}
onChange={(e) => onChangeItem(index, "quantity", e.target.value)}
/>
</td>
<td>{Number(tableItem.price) * Number(tableItem.quantity)}</td>
</tr>
</>
);
}
TableSheet
function TableSheet() {
const [tableItem, setTableItem] = useState([
{
price: 0,
quantity: 0
}
]);
const onChangeItem = (index, type, value) => {
const newTable = tableItem.map((item, idx) => {
if (idx === index)
return {
...item,
[type]: value
};
return item;
});
setTableItem(newTable);
};
const addCell = () => {
setTableItem((t) => [
...t,
{
price: 0,
quantity: 0
}
]);
};
const totalPrice = tableItem.reduce((acc, cur) => {
acc += Number(cur.price) * Number(cur.quantity);
return acc;
}, 0);
return (
<div>
<table>
<thead>
<th>Item Description</th>
<th>Price</th>
<th>Qty.</th>
<th>Total</th>
</thead>
{tableItem.map((tableItem, index) => {
return (
<TableItems
key={index}
index={index}
tableItem={tableItem}
onChangeItem={onChangeItem}
/>
);
})}
</table>
<button onClick={addCell}>+</button>
<div>Total: {totalPrice}</div>
</div>
);
}
you can check in my codesandbox. Hope it help!

How can I display changes in component instantly in React?

I'm building this website with MERN stack and having this rendering bug:
On start, I have a foodList table rendering out all of the food in the database.
I already have a useEffect() with the foodList inside the dependency array - so anytime the users make changes to the foodList table (Add/Edit/Delete), it will instantly render out that added dish without refreshing the page.
When users search for something in this Search & Filter bar, it will hide the foodList table and return a table of searchedFood that is filtered from the foodList array.
But when the users use this Search & Filter functionality and then try to Edit/Delete from that searchedFood table. It won't render the changes instantly - they have to refresh the page to see the changes they made.
This might relate to the useEffect() but I don't know how to apply it for the searchedFood table without disrupting the foodList table.
App.js
export default function App() {
const [foodName, setFoodName] = useState('')
const [isVegetarian, setIsVegetarian] = useState('no')
const [priceRange, setPriceRange] = useState('$')
const [foodUrl, setFoodUrl] = useState('')
const [foodList, setFoodList] = useState([])
const [searchedFood, setSearchedFood] = useState([])
const [noResult, setNoResult] = useState(false)
// Display food list:
useEffect(() => {
let unmounted = false
Axios.get("https://project.herokuapp.com/read")
.then((response) => {
if (!unmounted) {
setFoodList(response.data)
}
})
.catch(error => {
console.log(`The error is: ${error}`)
return
})
return () => {
unmounted = true
}
}, [foodList])
// Add Food to list:
const addToList = async (event) => {//Axios.post logic in here}
// Paginate states:
const [currentPage, setCurrentPage] = useState(1)
const [foodPerPage] = useState(5)
// Get current food:
const indexOfLastFood = currentPage * foodPerPage
const indexOfFirstFood = indexOfLastFood - foodPerPage
const currentFood = foodList.slice(indexOfFirstFood, indexOfLastFood)
const currentSearchedFood = searchedFood.slice(indexOfFirstFood, indexOfLastFood)
const paginate = (pageNumber) => {
setCurrentPage(pageNumber)
}
return (
<section>
<FilterSearch
foodList={foodList}
searchedFood={searchedFood}
setSearchedFood={setSearchedFood}
noResult={noResult}
setNoResult={setNoResult}
paginate={paginate}
/>
{noResult ? <ResultNotFound/>
:
<FoodListTable
foodName={foodName}
priceRange={priceRange}
isVegetarian={isVegetarian}
foodUrl={foodUrl}
foodList={foodList}
currentFood={currentFood}
searchedFood={searchedFood}
currentSearchedFood={currentSearchedFood}
totalFood={foodList.length}
totalSearchedFood={searchedFood.length}
currentPage={currentPage}
paginate={paginate}
noResult={noResult}
foodPerPage={foodPerPage}
/>
}
</section>
)
}
FoodListTable.js
export default function FoodListTable(props) {
return (
<div>
<table>
<thead>
<tr>
<th>
Food name
</th>
<th>Price</th>
<th>
Action
</th>
</tr>
</thead>
<body>
// Return a table with data from searchFood on search:
{props.searchedFood.length > 0 ? props.currentSearchedFood.map((val) => {
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
}) : props.currentFood.map((val) => { // If not on search, return a table with data from foodList:
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
})
}
</tbody>
</table>
// Display different Pagination on searched table and food list table:
{props.searchedFood.length > 0 ?
<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalSearchedFood} paginate={props.paginate} currentPage={props.currentPage} />
:<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalFood} paginate={props.paginate} currentPage={props.currentPage} />
}
</div>
)
}
FoodListRow.js
export default function FoodListRow(props) {
// Edit food name:
const [editBtn, setEditBtn] = useState(false)
const handleEdit = () => {
setEditBtn(!editBtn)
}
// Update Food Name:
const [newFoodName, setNewFoodName] = useState('')
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.catch(error => console.log(`The error is: ${error}`))
}
}
// Delete food:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
}
}
return (
<tr key={props.val._id}>
<td>
{props.val.foodName}
{editBtn &&
<div>
<input
type="text"
name="edit"
placeholder="New food name.."
autoComplete="off"
onChange={(event) => {setNewFoodName(event.target.value)}}
/>
<button
onClick={() => updateFoodName(props.val._id)}
>
✓
</button>
</div>
}
</td>
<td>{props.val.priceRange}</td>
<td>
<a
href={props.val.foodUrl}
target="_blank"
rel="noopener noreferrer"
>
🔗
</a>
<button
onClick={handleEdit}
>
✏️
</button>
<button
onClick={() => deleteFood(props.val._id)}
>
❌
</button>
</td>
</tr>
);
}
As Mohd Yashim Wong mentioned, we need to re-render every time there's change to the backend.
I ditched the foodList inside the useEffect()'s dependency array and try another method because this is not the correct way to re-render the axios calls. It just keeps sending read requests indefinitely if I use this way. That might be costly.
This is what I have switched to:
I set the dependency array empty
Pull the data from the backend and return it to the frontend after the axios calls
addToList function:
const addToList = async (event) => {
event.preventDefault()
try {
await Axios.post(
"https://project.herokuapp.com/insert",
{
foodName: foodName,
isVegetarian: isVegetarian,
priceRange: priceRange,
foodUrl: foodUrl,
}
)
.then((response) => {
// Return the data to the UI:
setFoodList([...foodList, { _id: response.data._id, foodName: foodName, isVegetarian: isVegetarian, priceRange: priceRange, foodUrl: foodUrl }])
setFoodName('')
setIsVegetarian('no')
setPriceRange('$')
setFoodUrl('')
})
} catch(err) {
console.error(`There was an error while trying to insert - ${err}`)
}
}
updateFoodName function:
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.then(() => {
// Update on searchedFood:
props.searchedFood.length > 0 ?
props.setSearchedFood(props.searchedFood.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
})) //Update on foodList
: props.setFoodList(props.foodList.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
}))
})
.catch(error => console.log(`Update name failed: ${error}`))
}
}
deleteFood function:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
.then(() => {
props.searchedFood.length > 0
? props.setSearchedFood(props.searchedFood.filter((val) => {
return val._id !== id
}))
: props.setFoodList(props.foodList.filter((val) => {
return val._id !== id
}))
})
}
}
You are never updating the text of the food name. Inside FoodListRow, you should create a state for the name of the food. Set this equal to props.val.foodName and then update it at the end of updateFoodName() after the axios request.

React Question: Undefined Values passing data into props

I have been struggling to find a solution to this problem. I'm getting TypeError: Cannot read property 'Department_ID' of undefined errors from props being passed in from parent state. The main problem I'm seeing is that the Departments data is not being passed to the child element. Please look at the code and tell me what I'm missing! Thank you in advance for whatever help you can give me!
Parent Component: Positions
import {useEffect, useState} from 'react'
import API_access from '../../API_access'
import Position from './Position';
function Positions() {
const [positions, setPositions] = useState([]);
const [departments, setDepartments] = useState([]);
const [_position_ID, setPositionID] = useState('');
const [_title, setTitle] = useState('');
const [_department_ID, setDepartmentID] = useState();
const [_department_name, setDepartmentName] = useState();
useEffect(() => {
const get_data = async() => {
const jobs = await API_access('/API/Positions', 'GET');
const depts = await API_access('/API/Departments', 'GET');
setPositions(jobs.data);
setDepartments(depts.data);
}
get_data();
},[])
const Create_Position = async () => {
let new_position = {
Position_ID: _position_ID,
Position_Title: _title,
Department_ID: _department_ID
}
const success = await API_access('/API/Positions', 'POST', new_position);
if (success) {
let tmpArray = positions;
tmpArray.push(new_position);
setPositions(tmpArray);
}
}
const Update_Position = async (position) => {
const success = await API_access('/API/Positions', 'PATCH', position);
if (success) {
let tmpArray = positions.filter(pos => pos.Position_ID !== position.Position_ID);
tmpArray.push(position);
setPositions(tmpArray);
}
}
const Delete_Position = async (position) => {
const success = await API_access('/API/Positions', position);
if(success) {
let tmpArray = positions.filter(pos => pos.Position_ID !== position.Position_ID);
setPositions(tmpArray);
}
}
const Set_Department_Data = (event) => {
setDepartmentName(event.target.options[event.target.selectedIndex].text);
setDepartmentID(event.target.value);
}
return (
<div>
<form>
<input type="text" name="txtPositionID" value={_position_ID} onChange={event => setPositionID(event.target.value)}/>
<label htmlFor="txtPositionID">Position ID</label>
<input type="text" name="txtTitle" value={_title} onChange={event => setTitle(event.target.value)}/>
<label htmlFor="txtTitle">Job Title</label>
<select value={_department_ID} text={_department_name} onChange={event => Set_Department_Data(event)}>
<option value="" disabled>Select Department</option>
{departments.map((department, index) => (
<option key={index} value={department.Department_ID}>{department.Department_Name}</option>
))}
</select>
<button onClick={() => Create_Position}>Save</button>
</form>
<table>
<thead>
<tr>
<td>Edit</td>
<td>Position ID</td>
<td>Job Title</td>
<td>Department</td>
<td>Delete</td>
</tr>
</thead>
<tbody>
{positions.length > 0 && positions.map((position, index) => (
<Position key={index} Position={position} Departments={departments} Update_Position={Update_Position} Delete_Position={Delete_Position}/>
))}
</tbody>
</table>
</div>
)
}
export default Positions
Child Element: Position
import {useState, useEffect} from 'react'
function Position({ Position, Departments, Update_Position, Delete_Position }) {
const [_department_ID, setDepartmentID] = useState(Position.Department_ID);
const [_department_name, setDepartmentName] = useState('');
const [_job_title, setJobTitle] = useState(Position.Position_Title);
const [_edit, toggleEdit] = useState(false);
useEffect(() => {
const set_data = () => {
console.log(Departments)
console.log(_department_ID)
let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
console.log(dept)
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name)
}
set_data();
}, [])
const Set_Department_Data = (event) => {
setDepartmentName(event.target.options[event.target.selectedIndex].text);
setDepartmentID(event.target.value);
}
const Update_This_Position = () => {
if (_edit) {
let pos = {
Position_ID: Position.Position_ID,
Position_Title: _job_title,
Department_ID: _department_ID
}
Update_Position(pos);
}
toggleEdit(!_edit);
}
if (_edit) {
return (
<tr>
<td><button onClick={() => Update_This_Position()}>Save</button></td>
<td>{Position.Position_ID}</td>
<td><input type="text" value={_job_title} onChange={event => setJobTitle(event.target.value)}/> </td>
<td><select value={_department_ID} text={_department_name} onChange={event => Set_Department_Data(event)}>
{Departments.map((department, index) => (
<option key={index} value={department.Department_ID}>{department.Department_Name}</option>
))}
</select></td>
<td><button onClick={() => Delete_Position(Position)}>X</button></td>
</tr>
)
} else {
return (
<tr>
<td><button onClick={() => Update_This_Position()}>></button></td>
<td>{Position.Position_ID}</td>
<td>{Position.Position_Title}</td>
<td>{_department_name}</td>
<td><button onClick={() => Delete_Position(Position)}>X</button></td>
</tr>
)
}
}
export default Position
Error:
TypeError: Cannot read property 'Department_ID' of undefined
set_data
src/components/Personnel/Position.js:15
12 | console.log(_department_ID)
13 | let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
14 | console.log(dept)
> 15 | setDepartmentID(dept.Department_ID);
| ^ 16 | setDepartmentName(dept.Department_Name)
17 | }
18 | set_data();
View compiled
(anonymous function)
src/components/Personnel/Position.js:18
15 | setDepartmentID(dept.Department_ID);
16 | setDepartmentName(dept.Department_Name)
17 | }
> 18 | set_data();
| ^ 19 | }, [])
20 |
21 | const Set_Department_Data = (event) => {
View compiled
▶ 16 stack frames were collapsed.
get_data
src/components/Personnel/Positions.js:18
15 | const jobs = await API_access('/API/Positions', 'GET');
16 | const depts = await API_access('/API/Departments', 'GET');
17 | setPositions(jobs.data);
> 18 | setDepartments(depts.data);
| ^ 19 | }
20 | get_data();
21 | },[])
View compiled
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error. Click the 'X' or hit ESC to dismiss this message.
Issue
Array.prototype.find can return undefined if no element passes the predicate condition.
The find() method returns the value of the first element in the
provided array that satisfies the provided testing function. If no
values satisfy the testing function, undefined is returned.
During the initial render cycle departments is an empty array, so dept is all but guaranteed to be undefined on the initial render.
const set_data = () => {
console.log(Departments)
console.log(_department_ID)
let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
console.log(dept)
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name)
}
Solution
You should check for valid results from the find before accessing the result.
const set_data = () => {
const deptId = String(_department_ID);
const dept = Departments.find(d => String(d.Department_ID) === deptId);
if (dept) {
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name);
}
}
BUT, this code is running in an useEffect hook with an empty dependency, so the hook runs only once when the component mounts. You've a couple options:
Add a dependency to the useEffect hook to set the component state when the Departments prop updates.
useEffect(() => {
const set_data = () => {
const deptId = String(_department_ID);
const dept = Departments.find(d => String(d.Department_ID) === deptId);
if (dept) {
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name);
}
}
set_data();
}, [Departments]); // <-- dependency
Conditionally render Position only when all of the props are populated. Ensure both positions and departments arrays have a truthy (non-zero) length.
<tbody>
{positions.length && departments.length && positions.map((position, index) => (
<Position
key={index}
Position={position}
Departments={departments}
Update_Position={Update_Position}
Delete_Position={Delete_Position}
/>
))}
</tbody>

Dynamically add and remove input elements with React

Say I have a table:
<div>
<tr>
<td>
<p id='1' className="foo"> Boom </p>
</td>
<td>
<p id='2' className="foo"> Bang </p>
</td>
<td>
<p id='3' className="foobar"> Pew Pew </p>
</td>
</tr>
</div>
I want the data inside it to be editable in-place. Thus I want to substitute <p> element with an <input> and then substitute it with <p> again, but with new value. I've been doing it with jQuery and now made it with what seems to me as plain JS but with React. Code looks like this:
import React, { Component } from "react";
import "./App.css";
class App extends Component {
handleClick(e) {
if (e.target.className === 'foo'){
let element = document.getElementById(e.target.id)
let element_value = element.innerText
let parent_element = element.parentNode
let new_element = document.createElement('input')
parent_element.removeChild(element)
parent_element.appendChild(new_element)
new_element.setAttribute('class', 'input')
new_element.setAttribute('id', e.target.id)
new_element.setAttribute('value', element_value)
} else if (e.target.className === 'input') {
let element = document.getElementById(e.target.id)
let element_value = element.value
let parent_element = element.parentNode
let new_element = document.createElement('p')
parent_element.removeChild(element)
parent_element.appendChild(new_element)
new_element.setAttribute('class', 'foo')
new_element.setAttribute('id', e.target.id)
new_element.innerText = element_value
}
};
componentDidMount() {
document.addEventListener('dblclick', this.handleClick)
}
componentWillUnmount() {
document.removeEventListener('dblclick', this.handleClick)
}
render() {
return (
<div>
<tr>
<td>
<p id='1' className="foo"> Boom </p>
</td>
<td>
<p id='2' className="foo"> Bang </p>
</td>
<td>
<p id='3' className="foobar"> Pew Pew </p>
</td>
</tr>
</div>
)
}
}
export default App
However this doesn't seem to me as a good practice. Could you give me hints on how to improve/change my approach? Thank you.
The best approach is probably to create a controlled component that handles all the logic for the editable cell, and store the values in the parent. I made a sandbox that you can check out here, but I'll add the code here as well.
That way the cell component provides all the view stuff needed, and the parent controls the logic and data for all the cells.
So, the editable cell handles the functionality of switching between views:
const EditableCell = ({ id, onEdit, className, value }) => {
const [isEditing, setIsEditing] = useState(false);
const onClick = useCallback(() => {
setIsEditing(true);
}, []);
const onFinishedEditing = useCallback(() => {
setIsEditing(false);
}, []);
const onKeyDown = useCallback(
(e) => {
if (e.key === "Enter") {
onFinishedEditing();
}
},
[onFinishedEditing]
);
return (
<td>
{isEditing ? (
<input
value={value}
onChange={(e) => onEdit(e.target.value, id)}
onBlur={onFinishedEditing}
onKeyDown={onKeyDown}
autoFocus
/>
) : (
<p {...{ id, className, onClick }}>{value}</p>
)}
</td>
);
};
And then the app stores the cells' data and renders an EditableCell for each one:
export default function App() {
// This stores the cells values and properties, you can
// add or remove cells here are needed
const [cellValues, setCellValues] = useState([
{ id: "1", class: "foo", value: "Boom" },
{ id: "2", class: "foo", value: "Bang" },
{ id: "3", class: "foobar", value: "Pew Pew" }
]);
const onEdit = (value, id) => {
setCellValues(
cellValues.map((cellVal) =>
cellVal.id === id ? { ...cellVal, value } : cellVal
)
);
};
return (
<div>
Click a cell to edit
<tr>
{cellValues.map((cellVal) => (
<EditableCell
id={cellVal.id}
value={cellVal.value}
className={cellVal.class}
onEdit={onEdit}
/>
))}
</tr>
</div>
);
}
This might not perfectly match with the functionality you're wanting, but should give you a starting point
I've been promise to myself that i will made good deed at least one per day.I know that you write in class way but i stick to hooks so much that... sorry man :P
it call onChange when during editing you will press enter.
import React, { Component, useEffect, useMemo, useRef, useState } from "react";
import "./styles.css";
const Td = ({ children, editable = false, onChange, className, id }) => {
const cell = useRef();
const [edit, setEdit] = useState(false);
const [value, setValue] = useState(() => {
while (typeof children !== "string") {
children = children.props.children;
}
return children;
});
const [oldValue, setOldValue] = useState(value);
useEffect(() => {
if (!cell.current) return;
const onEditMode = () => editable && setEdit(true);
const target = cell.current;
target.addEventListener("click", onEditMode);
return () => {
target.removeEventListener("click", onEditMode);
};
}, [cell, setEdit, editable]);
const paragraph = useMemo(() => (
<p id="1" className="foo">
{value}
</p>
),[value]);
const input = useMemo(() => {
const update = (value) => {
setEdit(false);
if (onChange && typeof onChange === "function") {
onChange({
id,
newValue: value,
oldValue: oldValue
});
setOldValue(value);
}
}
return (
<input
value={value}
onChange={ e => setValue(e.target.value)}
onKeyDown={ e => e.key === "Enter" && update(value)}/>
)
},[value, setEdit, onChange, id, oldValue, setOldValue]);
return (
<td ref={cell} className={className}>
{edit ? input : paragraph}
</td>
);
};
class App extends Component {
componentDidMount() {
}
componentWillUnmount() {
}
tableCellValueChange({ id, newValue, oldValue }) {
console.log(
`table cell id: ${id} value changed from ${oldValue} to ${newValue}`
);
}
render() {
return (
<div>
<table>
<thead></thead>
<tbody>
<tr>
<Td
onChange={this.tableCellValueChange}
id="special"
editable>
<p>Bang </p>
</Td>
<Td onChange={this.tableCellValueChange} editable>
<p>Bang</p>
</Td>
<Td editable={false} className="forbar">
Pew Pew
</Td>
</tr>
</tbody>
</table>
</div>
);
}
}
export default App;
here you have sandbox
live example

react not rerendering after state change

I know there have been similar questions, but I have a weird issue.
This is what I'm doing
import React, {useState} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
function App() {
const [pons, setPons] = useState();
const [translations, setTranslations] = useState([]);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('samochod');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('localhost:8080/pons/', {method: 'POST', body: {content: translationsToSave}});
if (resp.ok === true) {
setInputValue('');
setTranslations(null);
}
};
return (
<div className="App">
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{translations ? translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
const tmp = translationsToSave;
tmp.push(target);
setTranslationsToSave(tmp);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
const tmp = translationsToSave;
tmp.splice(tmp.findIndex(elem => elem === target));
setTranslationsToSave(tmp);
}}>
Remove translation
</Button>
}
</td>
</tr>
)
})))) : (
<div>No translations</div>
)}
</tbody>
</Table>
</div>
);
}
export default App;
So it's a basic app, it right now just adds and removes from an array wit setTranslationsToSave. After I click the Add translation button the view stays the same. But it refreshes when I click Enter edit mode. Same with Remove translation. I need to click Enter/Exit edit mode.
Hitting Translate also reloads the view. So the Add/Remove translation buttons are the only ones which do not refresh the page. Why? What am I missing?
The issue is that you are mutating the satte in Add/Remove translation button, so when react check before re-rendering if the state updater was called with the same state it feels that nothing has changed as it does a reference check and ehnce doesn't trigger re-render
Also while updating current state based on previous state use functional callback approach for state updater.
Update your state like below
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)); return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
In your Add translation click handler, you're mutating the state:
<Button onClick={() => {
// tmp is just a reference to state
const tmp = translationsToSave;
// You are mutating state, this will be lost
tmp.push(target);
setTranslationsToSave(tmp);
}}>
You should duplicate the state and add the new element:
<Button onClick={() => {
setTranslationsToSave([...translationsToSave, target]);
}}>

Categories

Resources