How to get around React setState Delay? - javascript

I have been stuck on the simple issue of the common React setState delay. I am currently looking to update an object within an array, by saving it to a state variable "newStud" within a child component, and pass it into a parent component to be utilized for a filtering function. My current issue is that state only updates completely after the second submission of an entry on my site. Thus, when the filter function in the parent component aims to read the array being passed in, it throws errors as the initial declaration of state is what is passed in. My question is if there is some way I can adjust for that delay in updating that information without having to break apart my larger components into smaller more manageable components?
For reference, here is the code I am utilizing for the child component (the issue is present in my "addTag" function):
import React, {useState, useEffect} from 'react';
import './studentcard.css';
import { Tags } from '../Tags/tags.js';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faPlus } from '#fortawesome/free-solid-svg-icons';
import { faMinus } from '#fortawesome/free-solid-svg-icons';
export function StudentCard({student, upStuds}) {
const [newStud, setNewStud] = useState({});
const [clicked, setClicked] = useState(false);
const [tag, setTag] = useState('');
// switches boolean to opposite value for plus/minus icon display
const onClick = () => {
setClicked(!clicked);
};
// triggers the addTag function to push a tag to the array within the student object
const onSubmit = async (e) => {
e.preventDefault();
await addTag(tag);
};
// captures the values being entered into the input
const onChange = (e) => {
setTag(e.target.value);
};
// this pushes the tag state value into the array that is located in the student object being passed down from the parent component
// it is meant to save the new copy of the "student" value in "newStuds" state variable, and pass that into the callback func
// ********** here is where I am experiencing my delay ************
const addTag = () => {
student.tags.push(tag);
setNewStud({...student});
upStuds(newStud);
setTag('');
};
let scores;
if (clicked !== false) {
scores = <ul className='grades-list'>
{student.grades.map((grade, index) => <li key={index} className='grade'>Test {(index + 1) + ':'} {grade}%</li>)}
</ul>;
}
return (
<div className='studentCard' >
<div className='pic-and-text'>
<img className='student-image' alt='' src={student.pic}></img>
<section className='right-side'>
<h3 id='names'>{student.firstName.toUpperCase() + ' ' + student.lastName.toUpperCase()}</h3>
<h4 className='indent'>Email: {student.email}</h4>
<h4 className='indent'>Company: {student.company}</h4>
<h4 className='indent'>Skill: {student.skill}</h4>
<h4 className='indent'>Average: {student.grades.reduce((a, b) => parseInt(a) + parseInt(b), 0) / student.grades.length}%</h4>
{scores}
<Tags student={student}/>
<form className='tag-form' onSubmit={onSubmit}>
<input className='tag-input' type='text' placeholder='Add a tag' onChange={onChange} value={tag}></input>
</form>
</section>
</div>
<FontAwesomeIcon icon={clicked !== false ? faMinus : faPlus} className='icon' onClick={onClick}/>
</div>
)
};
And if necessary, here is the Parent Component which is attempting to receive the updated information (the callback function I am using to fetch the information from the child component is called "upStuds") :
import React, {useState, useEffect} from 'react';
import './dashboard.css';
import {StudentCard} from '../StudentCard/studentcard';
import axios from 'axios';
export function Dashboard() {
const [students, setStudents] = useState([]);
const [search, setSearch] = useState('');
const [tagSearch, setTagSearch] = useState('');
useEffect(() => {
const options = {
method: 'GET',
url: 'https://api.hatchways.io/assessment/students'
};
var index = 0;
function genID() {
const result = index;
index += 1;
return result;
};
axios.request(options).then((res) => {
const students = res.data.students;
const newData = students.map((data) => {
const temp = data;
temp["tags"] = [];
temp["id"] = genID();
return temp;
});
setStudents(newData);
}).catch((err) => {
console.log(err);
});
}, []);
const onSearchChange = (e) => {
setSearch(e.target.value);
};
const onTagChange = (e) => {
setTagSearch(e.target.value);
};
// here is the callback function that is not receiving the necessary information on time
const upStuds = (update) => {
let updatedCopy = students;
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
};
// const filteredTagged = tagList.filter
return (
<div className='dashboard'>
<input className='form-text1' type='text' placeholder='Search by name' onChange={onSearchChange}></input>
<input className='form-text2' type='text' placeholder='Search by tag' onChange={onTagChange}></input>
{students.filter((entry) => {
const fullName = entry.firstName + entry.lastName;
const fullNameWSpace = entry.firstName + ' ' + entry.lastName;
if (search === '') {
return entry;
} else if (entry.firstName.toLowerCase().includes(search.toLowerCase()) || entry.lastName.toLowerCase().includes(search.toLowerCase())
|| fullName.toLowerCase().includes(search.toLowerCase()) || fullNameWSpace.toLowerCase().includes(search.toLowerCase())) {
return entry;
}
}).map((entry, index) => {
return (<StudentCard student={entry} key={index} upStuds={upStuds} />)
})}
</div>
)
};
Please let me know if I need to clarify anything! Thanks for any assistance!

setNewStud({...student});
upStuds(newStud);
If you want to send the new state to upStuds, you can assign it to a variable and use it twice:
const newState = {...student};
setNewStud(newState);
upStuds(newState);
Additionally, you will need to change your upStuds function. It is currently mutating the existing students array, and so no render will occur when you setStudents. You need to copy the array and edit the copy.
const upStuds = (update) => {
let updatedCopy = [...students]; // <--- using spread operator to create a shallow copy
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
}

Related

React Memo still rerenders on state update

I am wondering why the console.log({ routes }) is outputting on a search useState update without the search having filtered the data. I have memoized and useCallbacked things.
import { memo, useCallback, useMemo, useState } from 'react'
import * as stations from './stations.json'
import './App.css';
const StyledContainer = {
"cursor": "pointer",
"display": "flex",
"flex-direction": "row"
}
const StyledSegment = {
"justify-content": "space-between",
"text-align": "left",
"width": "33.3%"
}
const baseUrl = 'https://api.wheresthefuckingtrain.com/by-id'
const StationComponent = (fetchSchedule, s) => {
const { id, location, name, routes = false, stops } = s
console.log({ routes }) // THIS RERENDERS
return (
<div key={id} onClick={() => { fetchSchedule(id) }}>
<h3>{name}</h3>
<h4>{location}</h4>
{routes && routes.N.length > 0 && <div><label>North</label><ul>{routes.N.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
{routes && routes.N.length > 0 && <div><label>South</label><ul>{routes.S.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
</div>
)
}
const MemoizedStationComponent = memo(StationComponent)
function App() {
const [filteredStationData, setFilteredStationData] = useState(stations)
const [search, setSearch] = useState() // station name
const [stationData, setStationData] = useState(stations)
console.log({stations})
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
const copyStationData = JSON.parse(JSON.stringify(stations))
copyStationData[id].routes = json.data[0]
setStationData(copyStationData)
})
const filterStations = useCallback(() => {
const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search))
setFilteredStationData(filteredStations)
})
return (
<div className="App" style={StyledContainer}>
<header className="App-header">
<input placeholder="Name" onChange={(e) => { setSearch(e.target.value) }} /><button onClick={() => { filterStations() }}>Search</button>
<div style={StyledSegment}>
{Object.values(filteredStationData).map(s => <MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />)}
</div>
</header>
</div>
);
}
export default App;
Few points:
You haven't provided dependencies to useCallback.
Pass an inline callback and an array of dependencies. useCallback will
return a memoized version of the callback that only changes if one of
the dependencies has changed. This is useful when passing callbacks to
optimized child components that rely on reference equality to prevent
unnecessary renders (e.g. shouldComponentUpdate).
Also I notice you haven't passed keys here: <MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />
; this can become problematic (you should have received warning), and you can end up in corrupt state etc., if the elements of the array which you are mapping can reorder for example.
And general note, if you pass an object as prop to memoized component, make sure it is not recreated on each render, or it will break memoization.
Just add dependencies for useCallback to prevent unnecessary re-renders of your station component.
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
const copyStationData = JSON.parse(JSON.stringify(stations))
copyStationData[id].routes = json.data[0]
setStationData(copyStationData)
}, [stations])
const filterStations = useCallback(() => {
const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search))
setFilteredStationData(filteredStations)
}, [search])
Now each time your stations or search value is changed. Your component will re-render.

useEffect has a setState in it and is console.logging null

I will try to word this in the best way I can...
When I send a function through a prop to a child and then send it again to another child, then use the on click to activate it in the 'grandparent' function. When I console.log in that original function in the grandparent that console.logs a state, it prints undefined, yet when I am within that grandparent and try to activate that function, it will log the state correctly. If anyone can help me a little bit more in depth that would be great, we can call!
import React, { useEffect } from 'react';
import Row from '../row/row';
import './body.css';
import { nanoid } from 'nanoid';
export default function Body () {
const [boxMain, setBoxMain] = React.useState(null)
const [rows, setRows] = React.useState(null)
const ref = React.useRef(null)
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
React.useEffect(() => {
/* Describes array with all information */
const sideBoxes = 40;
const heightContainer = ref.current.offsetHeight
const widthContainer = ref.current.offsetWidth;
const numRows = Math.floor(heightContainer / sideBoxes) - 1
const numBoxes = Math.floor(widthContainer / sideBoxes)
/* Beginning of array birth */
let main = Array(numRows).fill().map(() => new Array(numBoxes).fill({
id: "",
water: false,
land: false,
air: false,
}));
/* End of array birth */
const rows = []
for (let i = 0; i < numRows; i++) {
const id = nanoid();
rows.push(
<Row
key={id}
id={id}
rowNumber={i}
numBoxes={numBoxes}
sideBoxes={sideBoxes}
changeBox={changeBox}
main={main}
/>
)
}
setRows(rows)
setBoxMain(main)
}, [])
return (
<div>
<div onClick={() => changeBox("test")}>
TEST
</div>
<div ref={ref} className='body'>
{rows}
</div>
</div>
)
}
For examples here onClick={() => changeBox("test") the function works and logs "boxMain" correctly. But when I pass changeBox={changeBox} into ...
import React, { useEffect } from "react";
import Box from "../box/box";
import "./row.css";
import { nanoid } from 'nanoid';
export default function Row (props) {
const ref = React.useRef(null)
const [boxes, setBoxes] = React.useState()
useEffect(() => {
const tempBoxes = []
for (let i = 0; i < props.numBoxes; i++) {
const id = nanoid()
tempBoxes.push(
<Box
rowNumber={props.rowNumber}
columnNumber={i}
key={id}
id={id}
side={props.sideBoxes}
changeBox={props.changeBox}
main={props.main}
/>
)
}
setBoxes(tempBoxes)
}, [])
return (
<div ref={ref} className="row-main">
{boxes}
</div>
)
}
Then pass changeBox={props.changeBox} to ...
import React from "react";
import "./box.css";
export default function Box (props) {
React.useEffect(() => {
props.main[props.rowNumber][props.columnNumber] = props.id
}, [])
const [detectChange, setDetectChange] = React.useState(0)
const ref = React.useRef(null)
const styles = {
width: `${props.side}px`,
height: `${props.side}px`,
}
return (
<div
ref={ref}
className="box-main"
key={props.id}
id={props.id}
rownumber={props.rowNumber}
columnnumber={props.columnNumber}
style={styles}
onClick={() => props.changeBox([props.id, props.rowNumber, props.columnNumber])}
>
</div>
)
}
I then have the onClick={() => props.changeBox([props.id, props.rowNumber, props.columnNumber])} and it returns to the original changeBox...
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
but when I click the box it returns the event correctly but returns boxMain as null.
When I click the onClick in the parent function although it console.logs everything correctly.
I know this is a ton of info but I know the fix has to be simple, or at least my method to do this should change.
Thank you for any feedback!! :)
Edit 1:
This is the output normally.
But when I simply add a space to the code and save it in VS Code (I guess some type of rerendering happens?) then it fixes to...
Although the IDs do change so I think everything refreshes in some way.
The useEffect hook of Body component runs only once because it does not have any dependency, thus changeBox callback passed to its children and grand children has the default state of boxMain, and it never updates.
This is why calling changeBox inside Body component logs boxMain array correctly, while calling props.changeBox inside children components logs null.
-------------- Solution ---------------------
This is not the BEST solution, but it will give you an idea why it didn't work before, and how you can fix it.
import React, { useEffect } from 'react';
import Row from '../row/row';
import './body.css';
import { nanoid } from 'nanoid';
export default function Body () {
const [boxMain, setBoxMain] = React.useState(null)
const [rows, setRows] = React.useState(null)
const [rowsData, setRowsData] = React.useState(null)
const ref = React.useRef(null)
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
React.useEffect(() => {
/* Describes array with all information */
const sideBoxes = 40;
const heightContainer = ref.current.offsetHeight
const widthContainer = ref.current.offsetWidth;
const numRows = Math.floor(heightContainer / sideBoxes) - 1
const numBoxes = Math.floor(widthContainer / sideBoxes)
/* Beginning of array birth */
let main = Array(numRows).fill().map(() => new Array(numBoxes).fill({
id: "",
water: false,
land: false,
air: false,
}));
/* End of array birth */
const rowsData = []
for (let i = 0; i < numRows; i++) {
const id = nanoid();
rowsData.push({
key: id,
id,
rowNumber: id,
numBoxes,
sideBoxes,
})
}
setRowsData(rowsData)
setBoxMain(main)
}, [])
React.useEffect(() => {
const rows = []
for (let i = 0; i < rowsData?.length; i++) {
const id = nanoid();
const data = rowsData[i];
rows.push(
<Row
{...data}
changeBox={changeBox}
main={boxMain}
/>
)
}
setRows(rows)
}, [rowsData, boxMain, changeBox])
return (
<div>
<div onClick={() => changeBox("test")}>
TEST
</div>
<div ref={ref} className='body'>
{rows}
</div>
</div>
)
}

useState not updating Grandparent state - data passes from Grandchild to child succesfully, but no further

I have an 'Autocomplete' grandchild React component which takes data from the child component, helps the user autocomplete a form field, and then passes the value right back up to the grandparent component which posts the whole form.
I am able to get data from grandchild to child, but not able to get the data from there up to the grandparent.
Grandparent - AddPost.jsx
import React, { useState } from 'react';
import GetBeerStyle from './GetBeerStyle';
export default function AddPost(props) {
const [parentBeerStyleData, setParentBeerStyleData] = useState("")
const handleSubmit = (e) => {
e.preventDefault();
// There's some code here that pulls the data together and posts to the backend API
}
return (
<div>
<GetBeerStyle
name="beerstyle"
beerStyleData={childData => setParentBeerStyleData(childData)}
onChange={console.log('Parent has changed')}
/>
// More data input fields are here...
</div>
);
}
Child - GetBeerStyle.jsx
import React, {useState, useEffect } from 'react';
import axios from 'axios';
import Autocomplete from '../Autocomplete';
export default function GetBeerStyle(props) {
const [beerStyleData, setBeerStyleData] = useState("")
const [beerStyles, setBeerStyles] = useState(null)
const apiURL = "http://localhost:8080/api/styles/"
// Code to fetch the beer styles from the API and push down to the grandchild
// component to enable autocomplete
const fetchData = async () => {
const response = await axios.get(apiURL)
const fetchedStyles = Object.values(response.data)
const listOfStyles = []
for (let i = 0; i < fetchedStyles.length; i++) {
(listOfStyles[i] = fetchedStyles[i].style_name)
}
setBeerStyles(listOfStyles)
};
// This snippet pulls data from the Posts table via the API when this function is called
useEffect(() => {
fetchData();
}, []);
return (
<div className="one-cols">
<Autocomplete
suggestions={ beerStyles } // sending the data down to gchild
parentUpdate={childData => setBeerStyleData(childData)}// passing data back to gparent
onChange={() => props.beerStyleData(beerStyleData)}
/>
</div>
);
}
Grandchild - Autocomplete.jsx
import React, { Component, Fragment, useState } from 'react';
export default function Autocomplete(props) {
const [activeSuggestion, setActiveSuggestion] = useState(0);
const [filteredSuggestions, setFilteredSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [userInput, setUserInput] = useState("");
const [fieldId, setFieldId] = useState("");
const [parentUpdate, setParentUpdate] = useState("");
const onChange = e => {
const { suggestions } = props;
setActiveSuggestion(0);
setFilteredSuggestions(suggestions.filter(suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) >-1));
setShowSuggestions(true);
setUserInput(e.currentTarget.value);
setParentUpdate(e.currentTarget.value);
(console.log(parentUpdate));
return props.parentUpdate(parentUpdate);
};
const onClick = e => {
setActiveSuggestion(0);
setFilteredSuggestions([]);
setShowSuggestions(false);
setUserInput(e.currentTarget.innerText);
setFieldId(props.fieldId);
setParentUpdate(e.currentTarget.innerText);
return props.parentUpdate(parentUpdate);
};
const onKeyDown = e => {
// User pressed the ENTER key
if (e.keyCode === 13) {
setActiveSuggestion(0);
setShowSuggestions(false);
setUserInput(filteredSuggestions[activeSuggestion]);
// User pressed the UP arrow
} else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
setActiveSuggestion(activeSuggestion - 1);
}
// User pressed the DOWN arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
setActiveSuggestion(activeSuggestion + 1);
}
};
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No Suggestions Available.</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
value={userInput}
onChange={onChange}
onKeyDown={onKeyDown}
id={fieldId}
/>
<div>
{suggestionsListComponent}
</div>
</Fragment>
);
}
While I certainly accept there may be other issues with the code, overriding problem that I seem to have spent an inordinate amount of time googling and researching, is that I can't get the data being entered in the form input, to pull through to the grandparent component!
What have I missed?

Clear button in React clears input but doesn't reset the array element

I try to make a simple meme generator where a user can add a text and change the image on click. Both is working but my clear-button only clears the input field and don't get back to the first image (array[o]).
I mean if I conole.log the "element" it says "0" but it don't change the image to the first one.
My code of App.js so far:
import React, { useEffect, useState } from "react";
import "./styles.css";
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
return { count, increment };
}
export default function App() {
let { count: element, increment } = useCounter(0);
const [memes, setMemes] = useState([]);
const [topText, setTopText] = useState("");
useEffect(() => {
async function asyncFunction() {
const initialResponse = await fetch("https://api.imgflip.com/get_memes");
const responseToJSON = await initialResponse.json();
setMemes(responseToJSON.data.memes);
}
asyncFunction();
}, []);
const clear = (e) => {
setTopText("");
element = 0;
console.log(element);
};
return (
<div className="App">
{memes[0] ? (
<div
style={{
height: "300px",
backgroundImage: `url(${memes[element].url})`
}}
>
<p>{topText}</p>
<input
value={topText}
onChange={(e) => setTopText(e.target.value)}
type="text"
/>
<button onClick={clear} type="reset">Clear</button>
<button onClick={increment}>Change Image</button>
</div>
) : (
"loading"
)}
</div>
);
}
What is wrong?
You are attempting to mutate state. You should never directly assign a new value to a stateful variable element = 0. You should use the provided updater function from useState (setCount).
One solution would be to add a reset function to your custom hook and use it:
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
const reset = () => setCount(initialCount);
return { count, increment, reset };
}
In your component:
const { count: element, increment, reset: resetCount } = useCounter(0);
const clear = (e) => {
setTopText("");
resetCount();
};
Notice I've also changed the custom hook to use a const instead of let. This is recommended to encourage immutable usage of state, and give helpful errors when breaking that rule.

How to remove an element from an array dynamically? React.js

There are two components, I want to implement an element array using the useContext hook, but when the button is clicked, the element is not removed, but on the contrary, there are more of them. Tell me what is wrong here. I would be very grateful!
First component:
import React from 'react';
import CartItem from './CartItem';
import Context from '../Context';
function Cart() {
let sum = 0;
let arrPrice = [];
let [products, setProducts] = React.useState([]);
let loacalProsucts = JSON.parse(localStorage.getItem('products'));
if(loacalProsucts === null) {
return(
<div className="EmptyCart">
<h1>Cart is empty</h1>
</div>
)
} else {
{loacalProsucts.map(item => products.push(item))}
{loacalProsucts.map(item => arrPrice.push(JSON.parse(item.total)))}
}
for(let i in arrPrice) {
sum += arrPrice[i];
}
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
return(
<Context.Provider value={{removeItem}}>
<div className="Cart">
<h1>Your purchases:</h1>
<CartItem products = {products} />
<h1>Total: {sum}$</h1>
</div>
</Context.Provider>
)
}
Second component:
import React, { useContext } from 'react';
import Context from '../Context';
function CartList({products}) {
const {removeItem} = useContext(Context);
return(
<div className="CartList">
<img src={products.image} />
<h2>{products.name}</h2>
<h3 className="CartInfo">{products.kg}kg.</h3>
<h2 className="CartInfo">{products.total}$</h2>
<button className="CartInfo" onClick={() => removeItem(products.id)}>×</button>
</div>
);
}
export default CartList;
Component with a context:
import React from 'react';
const Context = React.createContext();
export default Context;
Adding to the comment above ^^
It's almost always a mistake to have initialization expressions inside your render loop (ie, outside of hooks). You'll also want to avoid mutating your local state, that's why useState returns a setter.
Totally untested:
function Cart() {
let [sum, setSum] = React.useState();
const loacalProsucts = useMemo(() => JSON.parse(localStorage.getItem('products')));
// Init products with local products if they exist
let [products, setProducts] = React.useState(loacalProsucts || []);
useEffect(() => {
// This is actually derived state so the whole thing
// could be replaced with
// const sum = products.reduce((a, c) => a + c?.total, 0);
setSum(products.reduce((a, c) => a + c?.total, 0));
}, [products]);
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
...

Categories

Resources