I have some experience with class components in React, but am trying to learn hooks and functional components better.
I have the following code:
import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = styles;
newStyles.splice(index, 1);
setStyles(newStyles);
}
return (
<div>
{styles}
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
</div>
);
}
The goal of this code is to have an array of components that have an "x" button next to each one that removes that specific component, as well as a "+" button at the bottom that adds a new one. The StyleRow component just returns a paragraph JSX element right now.
The unusual behavior is that when I click the "x" button by a row, it removes the element and all elements following it. It seems that each new StyleRow component that is added takes the state of styles at the moment of its creation and modifies that instead of always modifying the current styles state. This is different behavior than I would expect from a class component.
The freezing of state leads me to believe this has something to do with closures, which I don't fully understand, and I am curious to know what here triggered them. If anyone knows how to solve this problem and always modify the same state, I would greatly appreciate it.
Finally, I think this post on SO is similar, but I believe it addresses a slightly different question. If someone can explain how that answer solves this problem, of course feel free to close this question. Thank you in advance!
You are modifying the existing state of styles, so you will need to create a deep copy of the array first.
You can either write your own clone function, or you can import the Lodash cloneDeep function.
Add the following dependency to your package.json using:
npm install lodash
Also, you are passing the length of the array to the removeStyle function. You should be passing the last index which is length - 1.
// ...
import { cloneDeep } from 'lodash';
// ...
function removeStyle(index) {
let newStyles = cloneDeep(styles); // Copy styles
newStyles.splice(index, 1); // Splice from copy
setStyles(newStyles); // Assign copy to styles
}
// ...
<Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...
If you want to use a different clone function or write your own, there is a performance benchmark here:
"What is the most efficient way to deep clone an object in JavaScript?"
I would also move the function assigned to the onClick event handler in the button outside of the render function. It looks like you are calling setStyles which adds a button with a removeStyle event which itself calls setStyles. Once you move it out, you may be able to better diagnose your issue.
Update
I rewrote your component below. Try to render elements using the map method.
import React, { useState } from "react";
import { Button } from "reactstrap";
const Controls = (props) => {
const [styles, setStyles] = useState([]);
const removeStyle = (index) => {
const newStyles = [...styles];
newStyles.splice(index, 1);
setStyles(newStyles);
};
const getChildNodeIndex = (elem) => {
let position = 0;
let curr = elem.previousSibling;
while (curr != null) {
if (curr.nodeType !== Node.TEXT_NODE) {
position++;
}
curr = curr.previousSibling;
}
return position;
};
const handleRemove = (e) => {
//removeStyle(parseInt(e.target.dataset.index, 10));
removeStyle(getChildNodeIndex(e.target.closest("div")));
};
const handleAdd = (e) => setStyles([...styles, styles.length]);
return (
<div>
{styles.map((style, index) => (
<div key={index}>
{style}
<Button data-index={index} onClick={handleRemove}>
×
</Button>
</div>
))}
<Button color="primary" onClick={handleAdd}>
+
</Button>
</div>
);
};
export default Controls;
I've added the most preferred way in a new answer as the previous one was becoming too long.
The explanation lies in my previous answer.
import React, { useState } from "react";
import { Button } from "reactstrap";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = [...styles]
newStyles.splice(index, 1);
setStyles(newStyles);
}
const addStyle = () => {
const newStyles = [...styles];
newStyles.push({content: 'ABC'});
setStyles(newStyles);
};
// we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
// this always ensures that the removeStyle callback has reference to the latest state at all times.
return (
<div>
{styles.map((style, index) => {
return (
<div>
<p>{style.content} - Index: {index}</p>
<Button onClick={() => removeStyle(index)}>x</Button>
</div>
);
})}
<Button color="primary" onClick={addStyle}>+</Button>
</div>
);
}
Here is a CodeSandbox for you to play around.
Let's try to understand what's going on here.
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
First render:
// styles = []
You add a new style.
// styles = [<div1>]
The remove callback from the div is holding the reference to styles, whose length is now 0
You add one more style. // styles = [<div1>, <div2>]
Since div1 was created previously and didn't get created now, its still holding a reference to styles whose length is still 0.
div2 is now holding a reference to styles whose length is 1.
Now the same goes for the removeStyle callback that you have. Its a closure, which means it's holding a reference to a value of its outer function, even after the outer function has done executing. So when removeStyles is called for the first div1 the following lines will execute:
let newStyles = styles; // newStyles []
newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged
setStyles(newStyles); // styles = [] (new state)
Now consider you have added 5 styles. So this is how the references will be held by each div
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];
So what happens if you try to remove div3, the following removeStyly will execute:
let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]
setStyles(newStyles); // styles = [div2, div2] (new state)
Hope that helps and addresses your concern. Feel free to drop any questions in the comments.
Here is a CodeSandbox for you to play around with and understand the issue properly.
Related
I made arr as a state and updated in Box.js on handleClick when I log it on console it updates the state perfectly. but doesn't render html
it is supposed to update arr and re-render all arr dependent variables
//Board.js
import React from 'react'
import Box from './Box'
import './Board.css'
function Board() {
//array of null grid
const [arr, setArr] = React.useState(Array(9).fill(null))
//check if current player is X
const [isXTurn, ,setIsXTurn] = React.useState(true)
//all props as object
const props={arr,setArr,isXTurn,setIsXTurn}
//array as Box element
const arrayEle = arr.map((ele,i)=>(<Box {...props} value={ele} index={i} key={i}/>))
return (
<div className='board'>
{arrayEle}
{arr}
</div>
);
}
export default Board;
//Box.js
import React from 'react'
import './Box.css'
function Box(props) {
//style as x & o
const style = props.value==='X'? 'box x': 'box o'
function handleClick(){
if(props.value)
return
console.log(props)
props.setArr(prevArr=>{
const updatedArray = prevArr
updatedArray[props.index] = (props.isXTurn?'X':'O')
return updatedArray
})
}
return (
<button className={style} onClick={handleClick}>
{props.value}
</button>
);
}
export default Box;
This is because you are mistakenly using setArr() function.
To invoke setArr() correctly in order to update state, you have to return a new value inside this function, not the same as previous one.
props.setArr(prevArr=>{
const updatedArray = prevArr
updatedArray[props.index] = (props.isXTurn?'X':'O')
return updatedArray
})
In your code up here, you assigned prevArr to the const updatedArray and return it. But you should know cloning object in a javascript is a bit tricky.
You just cloned prevArr shallowly. So internally, updatedArray is exactly still same value as prevArr. So even though you changed the keys or values inside of it, it only refers to the same value and that's because your setArr() doesn't recognize change.
So, you should clone deeply the prevArr and return it.
I just googled and attach a reference that would be helpful for you
The Best Way to Deep Copy an Object in JavaScript
I have a function which gets all the inputs rendered (querySelector) in order to execute other stuff and also is called by the "onChange" method of a component I have in order to detect if other inputs were added.
The main problem here is that when useEffect() is called, for some reason not all the inputs are detected, for example if in the DOM there are 3 inputs it only detects 2 (the last input added is not being detected as you can see in the example).
what I think is happening is that the querySelector function ends before the other input is rendered so thats why only the last one added is not being detected. I tried adding a setTimeout() function but that is not a good practice...
Any recommendations?
const onChangeQuery = (e) => {
//some other code here...
selectFirstOpt();
};
useEffect(() => {
selectFirstOpt();
},[])
const selectFirstOpt = () => {
let selects = document.querySelectorAll(".rule--field > select");
if(!selects.length) return;
selects.forEach((select) => {
let firstOpt = select.options[0];
if(firstOpt.value === "") firstOpt.innerText = "select an option";
})
}
I think you could use useRef with useEffect. Also using useRef you can work on the element instead of using selectors to change element values.
import React, { useRef, useEffect } from 'react';
...
/* In your react hook function*/
const first = useRef();
const second = useRef();
...
useEffect(() => {
first.current.value = "value";
second.current.value = "changed";
}, [first, second])
...
/* in your return function */
<input type="text" ref={first} />
<input type="text" ref={second} />
This will change the value of the inputs as soon as they are loaded correctly, it is important to add useRef variables or constants into useEffect square brackets.
How could I get the width of a JSX.Element? If I was in vanilla, I would do something like
const button = document.createElement('button');
document.body.appendChild(button)
window.getComputedStyle(button).width
Now I need to do the same, but it seems ref is null and I'm not sure even how to temporarily append to the DOM just to see what its width would be.
const button: JSX.Element = <CustomButton/>;
/// ....
Couple of things to check and consider.
where is the ref created and does it get forwarded correctly to a
valid dom element (make sure that CustomButton uses forwardRef).
you don't need to append anything in react to look at the width. all you need is ref.current.clientWidth or ref.current.getBoundingClientRect(), but ref.current has to exist in the first place :-)
if you need access to the ref.current element when your component first mounts (and not in a onClick or some other callback - then this does not apply) you'll have to use useLayoutEffect as the javascript runs before the dom is rendered so there is technically no to measure yet.
See this example:
ParentComponent.tsx
import {useState, useLayoutEffect} from 'react';
const ParentComponent = () => {
const [width, setWidth] = useState<number | null>(null);
useLayoutEffect(() => {
if(ref?.current && !width) {
const { clientWidth } = ref.current;
setWidth(clientWidth);
}
}, [ref?.current]);
console.log('width', width);
// `width` will be null at first render,
// then when CustomButton renders and <button> is created the ref will be
// updated, triggering your layout side effect that saves the
// clientWidth to the state. State change will trigger a rerender of
// ParentComponent and your console.log will finally print the width (whose
// value is stored in the state).
return <CustomButton ref={ref}/>;
};
CustomButton.tsx
import {forwardRef} from 'react';
const CustomButton = forwarRef((props, ref) => {
return (
<>
// some other stuff
<button ref={ref}/>>Click</button>
</>
);
};
The canonical way is to use a ref, and then observe it within an effect, which is called after the DOM gets rendered:
const ref = useRef();
useEffect(() => {
console.log(window.getComputedStyle(ref.current).width)
}, []);
return <button ref={ref}/>;
I have been learning hooks in react for the past couple of days, and I tried creating a scenario where I need to render a big grid on screen, and update the background color of the nodes depending on the action I want to take. There are two actions that will change the background color of a node, and these two actions must coexist.
The cursor hovers a node while it is clicked.
There exists an algorithm inside the Grid component that will change backgrounds of some of the
nodes.
The way I see it, there are multiple ways I can achieve this, but I am having some trouble with the way hooks were intended to be used. I will first walk you through my thought process on how this could be achieved from what I learned, and then show you the implementation that I tried. I tried to keep the important parts of the code so it can be understood clearly. Please let me know if I missed somethings or misunderstood a concept completely.
The children can hold their own state and know how to update themselves. The parent can hold the reference to each children of the list, and call the necessary function from the reference of the child when it is needed in order to update the children.
Works well for the first and the second action to be taken. This solution causes no performance issues since the children manage their own state, and if the parent updates the children state via reference, the only child to be re-rendered will be the one that gets called.
This solution is seen as an anti-pattern from what I read.
const Grid = () => {
// grid array contains references to the GridNode's
function handleMouseDown() {
setIsMouseDown(true);
}
function handleMouseUp() {
setIsMouseDown(false);
}
function startAlgorithm() {
// call grid[row][column].current.markAsVisited(); for some of the children in grid.
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
ref={grid[rowIndex][nodeIndex]}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = forwardRef((props, ref) => {
const [isVisited, setIsVisited] = useState(false);
useImperativeHandle(ref, () => ({
markAsVisited: () => {
setIsVisited(!isVisited);
}
}));
function handleMouseDown(){
setIsVisited(!isVisited);
}
function handleMouseEnter () {
if (props.isMouseDown.current) {
setIsVisited(!isVisited);
}
}
return (
<td id={`R${props.row}C${props.column}`}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
});
2. The state of the children could be given as props from the parent, any update operation can be achieved inside the parent. (Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter. If you move the mouse at a certain speed, nothing happens, and every visited node gets updated at once.)
Doesn't work for the first action. Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter. If you move the mouse at a certain speed, nothing happens and every visited node gets updated at once.
const Grid = () => {
// grid contains objects that have boolean "isVisited" as a property.
function handleMouseDown() {
isMouseDown.current = true;
}
function handleMouseUp() {
isMouseDown.current = false;
}
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid((grid) => {
const copyGrid = [...grid];
copyGrid[row][column].isVisited = !copyGrid[row][column].isVisited;
return copyGrid;
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
const {isVisited} = node;
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = ({row, column, isVisited, onMouseUp, onMouseDown, onMouseEnter}) => {
return useMemo(() => {
function handleMouseEnter() {
onMouseEnter(props.row, props.column);
}
return (
<td id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
}, [props.isVisited]);
}
I have two questions that I want to ask on this topic.
In the first implementation; the parent component doesn't re-render when a node changes its' state. Is it wrong to just utilize this anti-pattern if it is beneficial in this kind of situations?
What may be the cause of the stutter that the second implementation suffers from? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.
As you say that using refs to control child data is an anti-pattern, However it doesn't mean that you cannot use it.
What it means is that if there are better and more performant means, its better to use them as they lead to better readability of the code and also improve debugging.
In your case using a ref definitely makes it easier to update state and also prevents a lot of re-rendering is a good way to implement the above solution
What may be the cause of the stutter that the second implementation suffers from? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.
A lot of the problem in the second solution arise from the fact that you define functions which are recreated on each re-render and hence cause the entire grid to be re-rendered instead of just the cell. Make use of useCallback to memoize these function in Grid component
Also you should use React.memo instead of useMemo for your usecase in GridNode.
Another thing to note is that you are mutating the state while updating, Instead you should update it in an immutable manner
Working code:
const Grid = () => {
const [grid, setGrid] = useState(getInitialGrid(10, 10));
const isMouseDown = useRef(false);
const handleMouseDown = useCallback(() => {
isMouseDown.current = true;
}, []);
const handleMouseUp = useCallback(() => {
isMouseDown.current = false;
}, []);
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid(grid => {
return grid.map((r, i) =>
r.map((c, ci) => {
if (i === row && ci === column)
return {
isVisited: !c.isVisited
};
return c;
})
);
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{row.map((node, columnIndex) => {
const { isVisited } = node;
if (isVisited === true) console.log(rowIndex, columnIndex);
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
);
};
const GridNode = ({
row,
column,
isVisited,
onMouseUp,
onMouseDown,
onMouseEnter
}) => {
function handleMouseEnter() {
onMouseEnter(row, column);
}
const nodeVisited = isVisited ? "node-visited" : "";
return (
<td
id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={`node ${nodeVisited}`}
/>
);
};
P.S. While useCallback and other memoizations will help give to some performance benefits it will still not be able to overcome the performance impacts on state updates and re-render. In such scenarios its better to make define state within the children and expose a ref for the parent
As stated, the solution is an anti-pattern because you're mixing the rendering and business logic on both levels. You don't need to explicitly use React.forwardRef, in fact according to the docs you shouldn't, even when composing HOC (Higher order components). You shouldn't need to directly access the element and do some sort of action on it - let React do its thing. It's very good and efficient at it.
Generally when you're calling a re-render method on a child node tree when there's n nodes, you don't want to cause a re-render from the top-level node, the parent in this case, because it will cause the entire node-tree to re-render into a new DOM element, rather than update existing elements.
Your current solution has a combination of parent-triggered renders and child triggered renders. The React page has a good example with the tic-tac-toe application for how to render children without causing the parent to re-render.
The strategy that you should use is one where the parent node has an object structure, in this case n^2 nodes (eg 10x10 for arguments sake), is to pass the rendering functionality to the child nodes, and let the child nodes handle the rendering.
When you're triggering a render from the parent node, you have a couple of options (assuming functional components) which really fall into the case of observable updates. You want to be able to push updates from the parent to the child, to modify the child node state, and let the child node update itself.
Here's an example with child nodes rendering, while the parent is communicating changes to the children. You'll see that the performance scales well even up to massive grids, compared to the nested level renders your example has.
https://codepen.io/jmitchell38488/pen/pogbKEb
This is achieved by using a combination of RxJS observable/subject, React.useState and React.useEffect. We use useState in both the parent and child nodes to deal with rendering and prop updates, and useEffect to bind the observable. useState is persistent between renders, which means you don't need to rebuild the entire grid every time you update in the parent, but even if you do, React is intelligent enough to determine that you updated the props of a node, not replaced it.
const Grid = (props) => {
// When we update the grid, we trigger the parent to re-render
const [grid, setGrid] = React.useState([]);
const subject = new Rx.Subject();
if (grid.length < 1) {
const newGrid = [];
for (i = 0; i < props.h; i++) {
for (k = 0; k < props.w; k++) {
if (!Array.isArray(newGrid[i])) {
newGrid[i] = [];
}
newGrid[i][k] = {
visited: false,
id: `${i}${k}`
};
}
}
setGrid(newGrid);
}
// Tell our node to update
handleClick = (node, visited) => {
subject.next({
id: node.id,
visited: visited
})
};
randomSetAllVisited = () => {
const newGrid = [...grid];
newGrid.forEach(row => {
row.forEach(node => {
node.visited = Math.random() * 2 >= 1;
})
})
// Tell parent to re-render
setGrid(newGrid);
// Because our nodes use `useState`, they are persistent, if the structure of
// grid is the same and the data is mostly the same. This is based on the `key={...}` value
// in row.map, so we need to tell our children nodes to re-render manually
subject.next({
reset: true
})
};
randomSetAnyVisited = () => {
const h = Math.floor(Math.random()*props.h);
const w = Math.floor(Math.random()*props.w);
const node = grid[h][w];
subject.next({
id: node.id,
visited: true
});
};
// Watch console.log to see how frequently parent renders
console.log("rendering parent");
return (
<div>
<table>
<tbody>
{grid.map((row, rowIndex) => (
<tr key={`R${rowIndex}`}>
{row.map((node, columnIndex) => (<GridNode {...node} observer={subject.asObservable()} key={node.id} />))}
</tr>
))}
</tbody>
</table>
<button onClick={randomSetAllVisited}>Random set all visited</button>
<button onClick={randomSetAnyVisited}>Random set any visited</button>
</div>
);
};
const GridNode = (props) => {
// We need to set to undefined to handle full grid reset from parent
const [visited, setVisited] = React.useState(undefined);
// Toggle visited based on props and if visited is undefined
if (props.visited !== visited && visited === undefined) {
setVisited(props.visited);
}
// bind all this with useEffect, so we can subscribe/unsubscribe, and not block rendering, `useEffect` is a good practice
React.useEffect(() => {
// notifications that come from parent node, `setVisited` will re-render this node
const obs = props.observer.subscribe(next => {
if (!!next.id && next.id === props.id) {
setVisited(next.visited !== undefined ? next.visited : !visited);
} else if (!!next.reset) {
setVisited(undefined);
}
});
return () => obs.unsubscribe();
}, [visited]);
handleMouseEnter = () => {
setVisited(!visited);
}
handleMouseLeave = () => {
setVisited(!visited);
}
classes = ["node"];
if (visited) {
classes.push("node-visited");
}
return (<td onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className={classes.join(" ")}/>);
}
In the codepen example, I have a 50x50 grid, that has no stutters, lag, or issues re-rendering the children nodes, or updating them. There are two helper buttons to randomise the state for all nodes, or randomise a single node. I've scaled this over 100x100 and no lag or performance issues.
I am struggling to understand a strange behaviour while deleting an element from an array of divs.
What I want to do is create an array of divs representing a list of purchases. Each purchase has a delete button that must delete only the clicked one. What is happening is that when the delete button is clicked on the purchase x all the elements with indexes greather than x are deleted.
Any help will be appreciated, including syntax advices :)
import React, { useState } from "react";
const InvestmentSimulator = () => {
const [counter, increment] = useState(0);
const [purchases, setPurchases] = useState([
<div key={`purchase${counter}`}>Item 0</div>
]);
function addNewPurchase() {
increment(counter + 1);
const uniqueId = `purchase${counter}`;
const newPurchases = [
...purchases,
<div key={uniqueId}>
<button onClick={() => removePurchase(uniqueId)}>delete</button>
Item number {uniqueId}
</div>
];
setPurchases(newPurchases);
}
const removePurchase = id => {
setPurchases(
purchases.filter(function(purchase) {
return purchase.key !== `purchase${id}`;
})
);
};
const purchasesList = (
<div>
{purchases.map(purchase => {
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={purchases.indexOf(purchase)}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);
return <div>{purchasesList}</div>;
};
export default InvestmentSimulator;
There are several issues with your code, so I'll go through them one at a time:
Don't store JSX in state
State is for storing serializable data, not UI. You can store numbers, booleans, strings, arrays, objects, etc... but don't store components.
Keep your JSX simple
The JSX you are returning is a bit convoluted. You are mapping through purchases, but then also returning an add button if it is the last purchase. The add button is not related to mapping the purchases, so define it separately:
return (
<div>
// Map purchases
{purchases.map(purchase => (
// The JSX for purchases is defined here, not in state
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
// An add button at the end of the list of purchases
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
Since we should not be storing JSX in state, the return statement is where we turn our state values into JSX.
Don't give confusing names to setter functions.
You have created a state variable counter, and named the setter function increment. This is misleading - the function increment does not increment the counter, it sets the counter. If I call increment(0), the count is not incremented, it is set to 0.
Be consistent with naming setter functions. It is the accepted best practice in the React community that the setter function has the same name as the variable it sets, prefixed with the word "set". In other words, your state value is counter, so your setter function should be called setCounter. That is accurate and descriptive of what the function does:
const [counter, setCounter] = useState(0)
State is updated asynchronously - don't treat it synchronously
In the addNewPurchase function, you have:
increment(counter + 1)
const uniqueId = `purchase${counter}`
This will not work the way you expect it to. For example:
const [myVal, setMyVal] = useState(0)
const updateMyVal = () => {
console.log(myVal)
setMyVal(1)
console.log(myVal)
}
Consider the above example. The first console.log(myVal) would log 0 to the console. What do you expect the second console.log(myVal) to log? You might expect 1, but it actually logs 0 also.
State does not update until the function finishes execution and the component re-renders, so the value of myVal will never change part way through a function. It remains the same for the whole function.
In your case, you're creating an ID with the old value of counter.
The component
Here is an updated version of your component:
const InvestmentSimulator = () => {
// Use sensible setter function naming
const [counter, setCounter] = useState(0)
// Don't store JSX in state
const [purchases, setPurchases] = useState([])
const addNewPurchase = () => {
setCounter(prev => prev + 1)
setPurchases(prev => [...prev, { id: `purchase${counter + 1}` }])
}
const removePurchase = id => {
setPurchases(prev => prev.filter(p => p.id !== id))
}
// Keep your JSX simple
return (
<div>
{purchases.map(purchase => (
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
}
Final thoughts
Even with these changes, the component still requires a bit of a re-design.
For example, it is not good practice to use a counter to create unique IDs. If the counter is reset, items will share the same ID. I expect that each of these items will eventually store more data than just an ID, so give them each a unique ID that is related to the item, not related to its place in a list.
Never use indices of an array as key. Check out this article for more information about that. If you want to use index doing something as I did below.
const purchasesList = (
<div>
{purchases.map((purchase, i) => {
const idx = i;
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={idx}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);