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)
)
}
...
Related
I need React App level access via refs to my functions.
Specifically, Square > updateVal & updateColor. The hierarcy being App > Grid > Row x6 > Square x5. For whatever reason my useRef([]) array remains empty when it should contain 30 references (one to each Square). Hopefully the codebase will clarify my ask!
A secondary question of lesser importance: Sometimes my grid fails to display upon refreshing the page... if I change any of the text that's expected to render, the app finishes loading completely. I assume now that I've started to work with a useEffect() my issue might be related. Let me know if you notice anything that might relate to that issue as well.
// index.js
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// App.js
import React from 'react'
import Row from './Components/Row/Row';
import Square from './Components/Square/Square';
import { useEffect, useState, useRef } from 'react';
import Grid from './Components/Grid/Grid';
let hunch = ''
function App() {
const [hurdle, setHurdle] = useState('')
const [flag, setFlag] = useState('')
const MAX_GUESSES = 6
const MAX_WORD_LENGTH = 5
let guess = 0
let g = [...new Array(MAX_GUESSES)].map(a => new Array(MAX_WORD_LENGTH))
let hunchCharOccurrences = {}
const refs = useRef([])
let rows = new Array(MAX_GUESSES)
function handleEvent(event){
// ...
// OMITTED FOR BREVITY
// ...
let id = (MAX_GUESSES * guess) + hunch.length
if( refs && "current" in refs){
refs?.current[id].updateVal(k.toUpperCase())
refs?.current[id].setColor('palevioletred')
}
event.preventDefault()
}
function listen(){
document.addEventListener('keydown', handleEvent)
}
useEffect(() => {
refs.current = refs.current.slice(0, MAX_GUESSES * MAX_WORD_LENGTH)
let idIndex = 0
for(let i = 0; i < MAX_GUESSES; i++){
let squares = new Array(5);
for(let j = 0; j < MAX_WORD_LENGTH; j++){
squares[j] = <Square key={idIndex} ref={el => refs.current[idIndex] = el} />
idIndex++
}
rows[i] = squares
}
return listen()
}, [])
return (
<div className="App">
<main>
<div className='rendered-grid-container'>
<Grid rows={rows} />
</div>
<br/>
</main>
</div>
);
}
export default App;
// src>Components>Grid.js
import React, { useState } from 'react'
import Row from '../Row/Row'
function Grid({rows}, ref){
const gridItems = rows.map((row, index) =>
<tbody key={index}>
<Row squares={row} />
</tbody>
)
return (
<table className='grid-container'>
{ gridItems }
</table>
)
}
Grid.displayName = `Grid`
export default React.forwardRef(Grid)
// src>Components>Grid.js
import React, { useState } from 'react'
import Square from '../Square/Square'
function Row({squares}, ref){
const rowItems = squares.map((square, index) =>
<td key={index}>
{square}
</td>
)
return (
<tr className='row-container'>
{ rowItems }
</tr>
)
}
Row.displayName = `Row`
export default React.forwardRef(Row)
// src>Components>Square.js
import React, { useState, useImperativeHandle } from 'react'
function Square(props, ref) { // anonymouus -> named function
const [val, setVal] = useState('')
const [color, setColor] = useState('aqua')
function updateVal(v){
setVal(v)
}
function updateColor(c){
setColor(c)
}
useImperativeHandle(
ref,
() => {
return {
updateVal: updateVal,
updateColor: updateColor
}
},
)
return (
<div className='square-container'>
<div className='square' ref={ref} style={{backgroundColor: color}}>
{ val }
</div>
</div>
)
}
Square.displayName = `Square`
export default React.forwardRef(Square) //forwardRef HOC
I'm referencing the following posts as implementation guides:
How can I use multiple refs for an array of elements with hooks?
How to create dynamic refs in functional component- Using useRef Hook
React - Forwarding multiple refs
I'm aware that common React convention is passing data from children to their parent components. In addition to fixing my error, I'd appreciate some clarification on using functions for forwarded ref HOC's like mine. After all, my app itself is a F(x).
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>
)
}
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);
}
I am creating an array of categories in useEffect callback, it works fine when i console.log it.
But then when I .map() it, the resulting array is empty.
import React, { useEffect } from 'react';
export const Categories = (props) => {
let categories = [];
useEffect(() => {
props.films.forEach((film) => {
if (categories.findIndex(item => item === film.category) === -1)
{
categories.push(film.category);
console.log(categories);
}
})
}, [props.films, categories])
return (
<div>
{categories.map((category) => {
return (
<div>
{category}
</div>
)
})}
</div>
);
}
Does someone have an idea ?
You should use a state value for categories:
const [categories, setCategories] = React.useState([])
useEffect(() => {
let categories = []
props.films.forEach((film) => {
if (categories.findIndex(item => item === film.category) === -1)
{
categories.push(film.category);
console.log(categories);
}
})
setCategories(categories)
}, [props.films])
The component is not getting re-rendered when the data in categories is getting changed. In order to render the categories you need to store the data using useState.
import React, { useEffect, useState } from 'react';
export const Categories = (props) => {
const [categories, setCategories] = useState([]);
useEffect(() => {
let filmCategories = []
props.films.forEach((film) => {
if (categories.findIndex(item => item === film.category) === -1)
{
filmCategories.push(film.category);
console.log(filmCategories);
}
})
setCategories(filmCategories)
}, [props.films])
return (
<div>
{categories.map((category) => {
return (
<div>
{category}
</div>
)
})}
</div>
);
}
Hope this helps.
I think you maybe be new to react. I recommend you to take a look at React State and Lifecycle
You are using the react hook useEfect which will be called after your component is rendered in DOM
I can think of two possible solutions to solve this
1) using the react hook useState
import React, { useEffect } from 'react';
export const Categories = (props) => {
//Make the local variable into a variable representing state
let [categories, setCategories] = useState([]);
useEffect(() => {
const result = [...categories];
props.films.forEach((film) => {
if (result.findIndex(item => item === film.category) === -1)
{
result.push(film.category);
console.log(result);
}
})
//Set the state value to trigger a re-render of your component
if(result.length !== categories.length)
setCategories(result);
}, [props.films, categories])
return (
<div>
{categories.map((category) => {
return (
<div>
{category}
</div>
)
})}
</div>
);
}
2) If re-rendering is not required, remove the useEffect hook
import React, { useEffect } from 'react';
export const Categories = (props) => {
let categories = props.films.map(film => {
if (categories.findIndex(item => item === film.category) === -1)
{
categories.push(film.category);
console.log(categories);
}
}
return (
<div>
{categories.map((category) => {
return (
<div>
{category}
</div>
)
})}
</div>
);
}
If the useEffect react hook is required, then solution 1 is better
If there is no need to re-render the react component, then solution 2 is better
I created a custom toast component in my exercise React application. It is working correctly until the moment I try to introduce an auto dismiss timeout functionality. Basically when you load a new toast it needs to dismiss itself after let say 5000ms.
If you want check the full code in my Github Repo that also have a live preview.
Easiest way to create toast is put invalid mail / password.
I believe I am doing something wrong with the useEffect hook or I am missing something. The problem is that when I am creating multiple toasts they disappear all at the same time. Also React is complaining that I didn't include remove as a dependency of the useEffect hook but when I do it becomes even worse. Can someone demystify why this is happening and how it can be fixed. I am a bit new to React.
Here is the file that creates a HOC around my main App component:
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import ToastContext from './context';
import Toast from './Toast';
import styles from './styles.module.css';
function generateUEID() {
let first = (Math.random() * 46656) | 0;
let second = (Math.random() * 46656) | 0;
first = ('000' + first.toString(36)).slice(-3);
second = ('000' + second.toString(36)).slice(-3);
return first + second;
}
function withToastProvider(Component) {
function WithToastProvider(props) {
const [toasts, setToasts] = useState([]);
const add = (content, type = 'success') => {
const id = generateUEID();
if (toasts.length > 4) {
toasts.shift();
}
setToasts([...toasts, { id, content, type }]);
};
const remove = id => {
setToasts(toasts.filter(t => t.id !== id));
};
return (
<ToastContext.Provider value={{ add, remove, toasts }}>
<Component {...props} />
{ createPortal(
<div className={styles.toastsContainer}>
{ toasts.map(t => (
<Toast key={t.id} remove={() => remove(t.id)} type={t.type}>
{t.content}
</Toast>
)) }
</div>,
document.body
) }
</ToastContext.Provider>
);
}
return WithToastProvider;
}
export default withToastProvider;
And the Toast component:
import React, { useEffect } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => remove(), duration);
console.log(id);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;
Searching today for the solution I found it here
You will need to use useRef and its current property
Here is how I transformed the Toast component to work:
import React, { useEffect, useRef } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
const animationProps = useSpring({opacity: .9, from: {opacity: 0}});
const removeRef = useRef(remove);
removeRef.current = remove;
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => removeRef.current(), duration);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;