My question really simple: how do I create different instances of a React component?
I am doing an exercise in which you have to create a voting system: each component has its own quantity of votes.
The issue that I am having is that each component share the same number of votes, instead of being separate.
Here is the code:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
const Anecdote = ({text}) =>
{
const [votes, setVotes] = useState(0);
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => setVotes(votes + 1)}>vote</button>
</React.Fragment>
)
}
const App = (props) =>
{
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
setSelected(Math.floor(Math.random() * anecdotes.length));
}
return (
<div>
{props.anecdotes[selected]}
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
React.createElement(Anecdote, {text:'If it hurts, do it more often'}),
React.createElement(Anecdote, {text:'Adding manpower to a late software project makes it later!'}),
React.createElement(Anecdote, {text:'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.'}),
React.createElement(Anecdote, {text:'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.'}),
React.createElement(Anecdote, {text:'Premature optimization is the root of all evil.'}),
React.createElement(Anecdote, {text:'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.'}),
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
Basically, the function randomizeAnecdote() chooses a random anecdote to be displayed with its own text. However, even when displaying another anecdote, the votes don't change.
As an example, if one anecdote has 10 votes and I press the button to randomize, the 10 votes stay there.
How can I make it so that votes is unique to each element?
To reset the vote, you can listen on text in useEffect and whenever its changed, set vote to 0.
useEffect(() => {
setVotes(0)
}, [ text ])
Also, while testing I found an issue that random value is the same as previous value. So for that, you can use following hack:
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
Following is a sample code:
Note it addresses following things:
Reset vote count on new text.
Fixed Randomize function so no value is repeated
Updated code to save strings in array instead of React.Element
const { useState, useEffect } = React;
const Anecdote = ({text}) => {
const [votes, setVotes] = useState(0);
useEffect(() => {
setVotes(0)
}, [ text ])
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => setVotes(votes + 1)}>vote</button>
</React.Fragment>
)
}
const App = ({anecdotes}) => {
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
return (
<div>
<Anecdote text={ anecdotes[selected] } />
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id='root' />
Updated code to maintain the count:
The reason its resetting to 0 is because the useEffect is setting votes to 0 on change of text. If you need to maintain count, you will have to maintain a complex state.
In following sample, state is of type:
[ key: string ]: number
where key is the text and value is the count.
In ideal env, I would create a redux store that would maintain both values in more detailed fashion. But for sample, you can create a map<text, vote> and use it to display/maintain count.
const { useState, useEffect } = React;
const Anecdote = ({text}) => {
const [ myState, setMyState ] = useState({})
useEffect(() => {
if ( !myState[ text ] ) {
const state = { ...myState }
state[ text ] = 0;
setMyState(state);
}
}, [ text ])
const incrVote = () => {
const state = { ...myState }
state[ text ] = (state[ text ] || 0) + 1;
setMyState(state);
}
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {myState[ text ] || 0}</p>
<button onClick={incrVote}>vote</button>
</React.Fragment>
)
}
const App = ({anecdotes}) => {
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
return (
<div>
<Anecdote text={ anecdotes[selected] } />
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id='root' />
It seems a risky way to handle the count: I would never rely on the state of a "temporary" component for something important, since it makes things difficult for both persistency and tracking. I'm not familiar with useState, but obviously there is something wrong with the Javascript closures.
I'd separate data and components (you have together now), keeping track of the count in the higher component possible (App, in your case, if you switch to redux it simplifies things) and creating the Anecdote on the fly. That would be an easier to manage option, imho.
If I would write the code, I'd tackle it differently. That is subjective, so don't take it as correct or wrong (I don't call myself an expert at all), but I'll put some comments for my thoughts.
import React from 'react';
import ReactDOM from 'react-dom';
// Anecdote is simple, there is no state, only rendering
// <> is a shortcut for <React.Fragment>, can't use in StackOverflow
const Anecdote = ({text, votes, incVotes}) =>
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => incVotes()}>vote</button>
</React.Fragment>
// Data and components are separate, I don't merge them
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
// I'd go for a standard declaration of class, at least for the App
class App extends React.Component {
// I'm not familiar with useState (my bad), so here a classic init for the state
// (very verbose, I know)
constructor(props) {
super(props);
// Bogus: it starts with 0, can be fixed obvs
this.state = { selected: 0, votesCount: props.anecdotes.map(() => 0) };
}
// Your function, now external, I find it more readable. It could be improved.
randomizeAnecdote() {
const selected = Math.floor(Math.random() * anecdotes.length);
setState({ selected });
}
// I'd use the callback for the state, in case multiple click occurs and
// React groups the calls.
// Note that I copy the array, this will simplify the transition to redux.
// Using "immutable" behaviour is a good habit and simplifies debug.
incVotes() {
this.setState(prevState => {
const votesCount = [...prevState.votesCount];
votesCount[prevState.selected]++;
return({ ...prevState, votesCount });
});
}
// Much simpler render, there is no more array of Anecdote
render() {
return (
<div>
<Anecdote
text={this.props.anecdotes[selected]}
votes={this.state.votesCount[selected]}
incVotes={() => this.incVotes()}
/>
<br/>
<button onClick={() => this.randomizeAnecdote()}>press</button>
</div>
);
}
}
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
It might not reply to your answer (because I don't get which closure exactly is broken), but with an approach like above it should be easier to debug and maintain.
Related
I have an array of anecdotes and two buttons. One is to vote and the other is to randomly select the next anecdotes. When I click the vote button, the vote is seen as increasing numbers. However, the problem is that when I click on the next anecdotes, the vote remains instead of resetting back to the number zero for new anecdote.
I tried to store the vote numbers by first adding a new attribute Vote with an empty value "" to the array anecdotes, anecdotes.forEach(function (anecdote) { anecdote.Vote = "" }); and then pushing the increasing vote numbers to a new copy of an array const copyAnecdotes = [...anecdotes] but it didn't work. How can I make this work ?
App.js
import { useState } from 'react'
const inlineParagraph = {
display: 'inline-flex',
flexDirection: 'column',
}
const inlineButton = {
display: 'flex',
marginTop: '5px',
gap: '10px',
}
const Button = (props) => {
return (
<div>
<button onClick={props.handleClick}> {props.text} </button>
</div>
)
}
const App = () => {
const handleClick2 = () => {
setSelected(Math.floor(Math.random()*anecdotes.length))
}
const handleVote = () => {
setVote(vote + 1)
}
const anecdotes = [
'If it hurts, do it more often.',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
'Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients.',
'The only way to go fast, is to go well.'
]
const [selected, setSelected] = useState(0)
const [vote, setVote] = useState(0)
const copyAnecdotes = [...anecdotes]
return (
<div style={inlineParagraph}>
{ anecdotes[selected] }
<div style={inlineButton} >
<Button handleClick={handleClick2} text="next anecdotes" setSelected={setSelected} />
<Button handleClick={handleVote} text="vote" setSelected={setSelected} /> {vote}
</div>
</div>
)
}
export default App
First you need an array of votes:
const [votes, setVotes] = useState(() => Array(anecdotes.length).fill(0));
Then a function to add votes:
const handleVotes = () => {
setVote((votes) =>
votes.map((vote, index) => (index === selected ? vote + 1 : vote))
);
};
And display correct votes:
{votes[selected]}
You should reset the vote inside your handleClick2 function
const handleClick2 = () => {
setSelected(Math.floor(Math.random()*anecdotes.length));
setVote(0);
}
I am new to React and learning about states and props.
I am following a React Wes Bos course and the teacher is using class components, so I am sort of refactoring as I go along to functional component (for exercise and because I have to learn those).
We are coding an app that is supposed to be a fish restaurant, and at some point, we want to load to the order section some values.
I have two main problems:
1 - When I try to run the method addToOrder(key) manually in the React dev tool by using $r on App.js, I get an error
VM761:1 Uncaught TypeError: $r.addToOrder is not a function
2 - The second issue is that when I click on the button Add To Order, the one that is supposed to update the order{} object, the order object itself does not get updated.
I have been searching for a good half day now and I am not sure what could be wrong.
As a self-check:
the prop index is passed correctly from to as I can console.log(index) and do get the current one.
I am sorry if I am not explaining myself properly, it's a bit hard to condense into a short post. Do ask questions and clarifications as needed, I'll do my best to provide the correct info.
Here's the two components code:
App
import React from "react";
import { Header } from "./Header";
import { Order } from "./Order";
import { Inventory } from "./Inventory";
import { useState } from "react";
import sampleFishes from "../sample-fishes";
import { Fish } from "./Fish";
export const App = () => {
const [state, setState] = useState({
fishes: {},
order: {},
});
/**
* Structure of the function served in <AddFishForm>
* Making a copy of the state to avoid mutations ...state.fishes
* Date.now() used to assign a unique key
*
*/
const addFish = (fish) => {
const fishes = { ...state.fishes };
fishes[`fish${Date.now()}`] = fish;
setState({
fishes: fishes,
});
};
/**
* Function to display a sample fishes in the list
* Made to avoid manual typing
* Fish data comes from ../sample-fishes
*/
const loadSampleFishes = () => {
setState({ fishes: sampleFishes });
};
/**
* Take a copy of state
* Either add to the order or update the number in order
* (if order exists, adds one to it, if not, set it to one)
* Call setState() to update state object
*/
const addToOrder = (key) => {
const order = { ...state.order };
order[key] = order[key] + 1 || 1;
setState({
order: order,
});
};
return (
<div className="catch-of-the-day">
<div className="menu">
<Header tagline="Fresh Seafood Market" />
<ul className="fishes">
{Object.keys(state.fishes).map((key) => {
return (
<Fish
key={key}
details={state.fishes[key]}
addToOrder={addToOrder}
></Fish>
);
})}
</ul>
</div>
<Order />
<Inventory addFish={addFish} loadSampleFishes={loadSampleFishes} />
</div>
);
};
Fish
import React from "react";
import { formatPrice } from "../helpers";
export const Fish = ({ details, addToOrder, index }) => {
const isAvailable = details.status === "available";
const handleClick = () => {
addToOrder[index];
};
return (
<li className="menu-fish">
<img src={details.image} alt="" />
<h3 className="fish-names">
{details.name}
<span className="price">{formatPrice(details.price)}</span>
</h3>
<p>{details.desc}</p>
<button type="submit" disabled={!isAvailable} onClick={() => handleClick}>
{isAvailable ? "Add to order" : "Sold out!"}
</button>
</li>
);
};
calling the function from $r
TL;DR Solution
Now that I know what I am looking for, this was the issue: updating and merging an object using React useState hook.
I was missing to copy the previous state when updating order{}
The rest was pretty much correct, so the bit of code with the improvement is:
const addOrder = (key) => {
const order = { ...state.order };
order[key] = order[key] + 1 || 1;
setState({
...state,
order: order,
});
};
This post (as well as the last answer on this one) really explains it well: https://stackoverflow.com/a/61243124/20615843
This is the relative bit in the React docs:https://reactjs.org/docs/hooks-reference.html#functional-updates
Apparently, and even better practice is using useReducer() as stated: https://stackoverflow.com/a/71093607/20615843
The onClick={() => handleClick} should be
either with parenthesis at the end to call it
onClick={() => handleClick()}
or better yet, pass it directly as the callback method
onClick={handleClick}
First welcome to react world Jim Halpert! Hope you are enjoying your journey.
Their a couple of issues I have found in your example.
1)In the Fish.jsx click handler you need to pass the index while calling the actual function.
2)You have to bind the index as props, from the parent JSX file.
3)Since you have order and fishes in the same object you will have to copy the previous data as well as shown in line 57 of App.jsx
I have tweaked your example a bit, have a look at it below:
import React from "react";
import { useState } from "react";
import { Fish } from "./Fish";
const App = () => {
const [state, setState] = useState({
fishes: {
"salmon" : {
"image":"https://via.placeholder.com/20x20",
"name":"salmon",
"description":"test",
"status":"available"
}
},
order: {},
});
/**
* Structure of the function served in <AddFishForm>
* Making a copy of the state to avoid mutations ...state.fishes
* Date.now() used to assign a unique key
*
*/
const addFish = (fish) => {
const fishes = { ...state.fishes };
fishes[`fish${Date.now()}`] = fish;
setState({
fishes: fishes,
});
};
/**
* Function to display a sample fishes in the list
* Made to avoid manual typing
* Fish data comes from ../sample-fishes
*/
const loadSampleFishes = () => {
setState({ fishes: sampleFishes });
};
/**
* Take a copy of state
* Either add to the order or update the number in order
* (if order exists, adds one to it, if not, set it to one)
* Call setState() to update state object
*/
const addToOrder = (fish) => {
const order = { ...state.order };
order[fish] = order[fish.key] + 1 ?? 1;
console.log("Order >>>>>>>>>>>> "+JSON.stringify(order));
setState({
...state,
order: order,
});
};
return (
<div className="catch-of-the-day">
<div className="menu">
<ul className="fishes">
{Object.keys(state.fishes).map((key) => {
return (
<Fish
index={key}
details={state.fishes[key]}
addToOrder={addToOrder}
></Fish>
);
})}
</ul>
</div>
</div>
);
};
export default App;
import React from "react";
export const Fish = ({ details, addToOrder, index }) => {
const isAvailable = details.status === "available";
const handleClick = (index) => {
addToOrder(index);
};
return (
<li className="menu-fish">
<img src={details.image} alt="" />
<h3 className="fish-names">
{details.name}
<span className="price">{details.price}</span>
</h3>
<p>{details.desc}</p>
<button type="submit" disabled={!isAvailable} onClick={() => handleClick(index)}>
{isAvailable ? "Add to order" : "Sold out!"}
</button>
</li>
);
};
This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 5 months ago.
I'm working on building a file upload portal in React that allows for multiple concurrent uploads, using an amalgamation of some preexisting code and new code that I'm writing myself. The code base is pretty complex, so I will explain what's happening at a high level, illustrate the problem, and then provide a toy example to simulate what's happening in CodeSandbox.
The top level component has a files state variable from useState, which is an object that contains sub objects with information regarding each file that the user is currently uploading, that is being mapped across in the JSX and returning UI elements for each. Think:
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
},
2: {
id: 2,
name: "File 2",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const progressElements = Object.values(files).map((file) => (
<Progress key={file.id} value={file.progress} />
))
When an upload is initiated, existing code dictates that a callback is provided from the top level that receives an updated progress value for a given upload, and then sets that into state in files for the corresponding upload. This works perfectly fine when there is only one active upload, but as soon as a second file is added, the fact that the same files state object is being concurrently updated in multiple places at once bugs out the UI and causes the rendered JSX to be inaccurate. What is the correct way to handle concurrent updates to the same state at once?
Below is a super simplified (and hastily written, my apologies) sandbox as a toy example of what's going on. It's obviously not an exact replica of what's happening in the actual code, but it gets the general idea across. You can see that, with one upload going, the UI updates fine. But when additional uploads are added, any updates to the first overwrite the existence of the new upload in state and thus break the UI.
https://codesandbox.io/s/elastic-wozniak-yvbfo6?file=/src/App.js:133-297
const { useState, useEffect } = React;
const App = () => {
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const [uploading, setUploading] = useState(false);
const updateProgress = (uploadId, progress) => {
setFiles({
...files,
[uploadId]: {
...files[uploadId],
progress
}
});
};
const filesArray = Object.values(files);
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles({
...files,
[newId]: {
id: newId,
name: 'File ' + newId,
progress: 0
}
});
};
return (
<div className="App">
{filesArray.map((file) => (
<UploadStatus
key={file.id}
uploading={uploading}
setUploading={setUploading}
file={file}
updateProgress={updateProgress}
/>
))}
<button
onClick={
uploading ? () => setUploading(false) : () => setUploading(true)
}
>
{uploading ? "Cancel Upload" : "Start Upload"}
</button>
{uploading && <button onClick={addNewFile}>Add New File</button>}
</div>
);
}
const UploadStatus = ({
file,
updateProgress,
uploading,
setUploading
}) => {
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgress(file.id, calls * 10);
}, 1000);
if (calls === 10) {
clearInterval(interval);
setUploading(false);
}
return () => clearInterval(interval);
}, [uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<body>
<div id="root"></div>
</body>
Any help or thoughts would be greatly appreciated. Thanks!
Based on what is inside your codesandbox - the issue is just with closures and missing items in dependency array of useEffect. I do understand why you want to have only the things you described in depsArray of useEffect - but if you dont provide all the things there - you will get an issue with closure and you will be calling the old function or refering old variable, what is happening in your codesandbox. Consider wrapping everything that is passed to the hooks or child components with useMemo, useCallback and other memoizing functions react provide and include everything that is needed in depsArray. There is a workaround with useRef hook to hold a reference to the specific function you want to call + useEffect with the only purpose to update this useRef variable.
So rough fix 1:
import { useEffect, useRef } from "react";
export default function UploadStatus({
file,
updateProgress,
uploading,
setUploading
}) {
const updateProgressRef = useRef(updateProgress);
const setUploadingRef = useRef(setUploading);
useEffect(() => {
updateProgressRef.current = updateProgress;
}, [updateProgress]);
useEffect(() => {
setUploadingRef.current = setUploading;
}, [setUploading]);
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgressRef.current(file.id, calls * 10);
if (calls === 10) {
clearInterval(interval);
setUploadingRef.current(false);
}
}, 1000);
return () => clearInterval(interval);
}, [file.id, uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
You will have to fix some logic here due to when any of the Uploadings is done - all the rest uploading will "stop" due to uploading and setUploading parameters.
What can simplify the fixing process is to modify addNewFile and updateProgress so they will not rely on closure captured files. setXXX functions from useState can recieve either the new value as a parameter, either a callback currentValue => newValue. So you can use callback one:
So rough fix 2:
const updateProgress = (uploadId, progress) => {
setFiles((files) => ({
...files,
[uploadId]: {
...files[uploadId],
progress
}
}));
};
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles((files) => ({
...files,
[newId]: {
id: newId,
name: `File ${newId}`,
progress: 0
}
}));
};
This fix will actually work better but still, wirings of states and components and depsArray should be fixed more precisiolly.
Hope that helps you to get the basic idea of what is going on and in which direction to dig with your issues.
I'm trying to update a react state that holds nested values. I want to update data that is 3 levels deep.
Here is the state that holds the data:
const [companies, setCompanies] = useState(companies)
Here is the data for the first company (the companies array holds many companies):
const companies = [
{
companyId: 100,
transactions: [
{
id: "10421A",
amount: "850",
}
{
id: "1893B",
amount: "357",
}
}
]
Here is the code for the table component:
function DataTable({ editCell, vendors, accounts }) {
const columns = useMemo(() => table.columns, [table]);
const data = useMemo(() => table.rows, [table]);
const tableInstance = useTable({ columns, data, initialState: { pageIndex: 0 } }, useGlobalFilter, useSortBy, usePagination);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
rows,
page,
state: { pageIndex, pageSize, globalFilter },
} = tableInstance;
return (
<Table {...getTableProps()}>
<MDBox component="thead">
{headerGroups.map((headerGroup) => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<DataTableHeadCell
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
width={column.width ? column.width : "auto"}
align={column.align ? column.align : "left"}
sorted={setSortedValue(column)}
>
{column.render("Header")}
</DataTableHeadCell>
))}
</TableRow>
))}
</MDBox>
<TableBody {...getTableBodyProps()}>
{page.map((row, key) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
cell.itemsSelected = itemsSelected;
cell.editCell = editCell;
cell.vendors = vendors;
cell.accounts = accounts;
return (
<DataTableBodyCell
noBorder={noEndBorder && rows.length - 1 === key}
align={cell.column.align ? cell.column.align : "left"}
{...cell.getCellProps()}
>
{cell.render("Cell")}
</DataTableBodyCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
)
}
For example, I want to update the amount in the first object inside the transactions array. What I'm doing now is update the entire companies array, but doing this rerenders the whole table and creates problems. Is there a way I can only update the specific value in a manner that rerenders just the updated field in the table without rerendering the whole table? I've seen other answers but they assume that all values are named object properties.
FYI, I'm not using any state management and would prefer not to use one for now.
You have to copy data (at least shallow copy) to update state:
const nextCompanies = { ...companies };
nextCompanies.transactions[3].amount = 357;
setState(nextCompanies);
Otherwise react won't see changes to the original object. Sure thing you can use memoization to the child component to skip useless rerenders. But I strongly recommend to provide an optimisation only when it is needed to optimise. You will make the code overcomplicated without real profit.
When updating state based on the previous state, you probably want to pass a callback to setCompanies(). For example:
setCompanies((currCompanies) => {
const nextCompanies = [...currCompanies];
// modify nextCompanies
return nextCompanies;
})
Then, in order for React to only re-render the elements that changed in the DOM, you should make sure to set the key prop in each of those elements. This way, React will know which element changed.
// inside your component code
return (
<div>
companies.map(company => (
<Company key={company.id} data={company} />
))
</div>
)
Does this solve the problem? If not, it may be helpful to add some more details so we can understand it fully.
What I'm doing now is update the entire companies array, but doing
this rerenders the whole table and creates problems.
When you say it creates problems what type of problems exactly? How does re-rendering create problems? This is expected behavior. When state or props change, by default a component will re-render.
You seem to be asking two questions. The first, how to update state when only modifying a subset of state (an amount of a transaction). The second, how to prevent unnecessary re-rendering when render relies on state or props that hasn't changed. I've listed some strategies for each below.
1. What is a good strategy to update state when we only need to modify a small subset of it?
Using your example, you need to modify some data specific to a company in a list of companies. We can use map to iterate over each company and and conditionally update the data for the company that needs updating. Since map returns a new array, we can map over state directly without worrying about mutating state.
We need to know a couple things first.
What transaction are we updating?
What is the new amount?
We will assume we also want the company ID to identify the correct company that performed the transaction.
We could pass these as args to our function that will ultimately update the state.
the ID of the company
the ID of the transaction
the new amount
Any companies that don't match the company ID, we just return the previous value.
When we find a match for the company ID, we want to modify one of the transactions, but return a copy of all the other previous values. The spread operator is a convenient way to do this. The ...company below will merge a copy of the previous company object along with our updated transaction.
Transactions is another array, so we can use the same strategy with map() as we did before.
const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
setCompanies(() => {
return companies.map((company) => {
return company.id === companyId
? {
...company,
transactions: company.transactions.map((currTransaction) => {
return currTransaction.id === transactionId
? {
id: currTransaction.id,
amount: newAmount
}
: currTransaction;
})
}
: company;
});
});
};
2. How can we tell React to skip re-rendering if state or props hasn't changed?
If we are tasked with skipping rendering for parts of the table that use state that didn't change, we need a way of making that comparison within our component(s) for each individual company. A reasonable approach would be to have a reusable child component <Company /> that renders for each company, getting passed props specific to that company only.
Despite our child company only being concerned with its props (rather than all of state), React will still render the component whenever state is updated since React uses referential equality (whether something refers to the same object in memory) whenever it receives new props or state, rather than the values they hold.
If we want to create a stable reference, which helps React's rendering engine understand if the value of the object itself hasn't changed, the React hooks for this are useCallback() and useMemo()
With these hooks we can essentially say:
if we get new values from props, we re-render the component
if the values of props didn't change, skip re-rendering and just use the values from before.
You haven't listed a specific problem in your question, so it's unclear if these hooks are what you need, but below is a short summary and example solution.
From the docs on useCallback()
This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
From the docs on useMemo()
This optimization helps to avoid expensive calculations on every render.
Demo/Solution
https://codesandbox.io/s/use-memo-skip-child-update-amount-vvonum
import { useState, useMemo } from "react";
const companiesData = [
{
id: 1,
transactions: [
{
id: "10421A",
amount: "850"
},
{
id: "1893B",
amount: "357"
}
]
},
{
id: 2,
transactions: [
{
id: "3532C",
amount: "562"
},
{
id: "2959D",
amount: "347"
}
]
}
];
const Company = ({ company, onChangeAmount }) => {
const memoizedCompany = useMemo(() => {
console.log(
`AFTER MEMOIZED CHECK COMPANY ${company.id} CHILD COMPONENT RENDERED`
);
return (
<div>
<p>Company ID: {company.id}</p>
{company.transactions.map((t, i) => {
return (
<div key={i}>
<span>id: {t.id}</span>
<span>amount: {t.amount}</span>
</div>
);
})}
<button onClick={onChangeAmount}> Change Amount </button>
</div>
);
}, [company]);
return <div>{memoizedCompany}</div>;
};
export default function App() {
const [companies, setCompanies] = useState(companiesData);
console.log("<App /> rendered");
const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
setCompanies(() => {
return companies.map((company) => {
return company.id === companyId
? {
...company,
transactions: company.transactions.map((currTransaction) => {
return currTransaction.id === transactionId
? {
id: currTransaction.id,
amount: newAmount
}
: currTransaction;
})
}
: company;
});
});
};
return (
<div className="App">
{companies.map((company) => {
return (
<Company
key={company.id}
company={company}
onChangeAmount={() =>
handleChangeAmount({
companyId: company.id,
transactionId: company.transactions[0].id,
newAmount: Math.floor(Math.random() * 1000)
})
}
/>
);
})}
</div>
);
}
Explanation
On mount, the child component renders twice, once for each company.
The button will update the amount on the first transaction just for that company.
When the button is clicked, only one <Company /> component will render while the other one will skip rendering and use the memoized value.
You can inspect the console to see this in action. Extending this scenario, if you had 100 companies, updating the amount for one company would result in 99 skipped re-renders with only one new component rendering for the updated company.
I am pretty a new to React and currently to use the current state of and object as a variable in another array, said array will start filled with 0, and every time someone press the vote button it will store +1 to said array index. I am sure its the wrong way but nevertheless I amtrying to figure out if is possible to use the logic i created.
Thanks for the patience!
import React, { useState } from 'react'
const App = () => {
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
'Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients'
]
const [selected, setSelected] = useState(0)
var ary = new Uint8Array(10);
//console.log('this', this.state);
//show 1 anecdote
//when user press next its generates a random number beetween 0 and 6 to display the anecdote
//issue:
//how to add said random value to my arrays so I can use the array to store the votes of each anecdote
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button onClick={ () => setSelected(Math.floor(Math.random() * 6) ) }>Next Anecdote</button>
<button onClick={ () => ary[selected.state] + 1 }>Vote</button>
<p>votes: {ary[selected.state]}</p>
</div>
)
}
export default App
First, you'll need an array to hold the vote count values, and secondly, correctly update each vote count in an immutable update.
export default function App() {
const [selected, setSelected] = useState(0);
// create vote counts array from anecdotes array and initialize to zeros
const [votes, setVotes] = useState(Array.from(anecdotes).fill(0));
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button
onClick={() => setSelected(
// use anecdote array length
Math.floor(Math.random() * anecdotes.length))
}
>
Next Anecdote
</button>
<button
onClick={() =>
setVotes((votes) =>
// immutable update, map previous state to next
votes.map((count, index) =>
index === selected ? count + 1 : count
)
)
}
>
Vote
</button>
<p>votes: {votes[selected]}</p> // display selected anecdote vote count
</div>
);
}
All values which you change in React should be reactive, you never should change a value directly, because it will not trigger re-rendering. You should use useState hook.
In your case to store the anecdotes votes you could create a new array with length 6 and fill it with initial votes count - 0. Then you should call the hook to update the counts.
const [votes, setVotes] = useState(new Array(6).fill(0));
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button onClick={ () => setSelected(Math.floor(Math.random() * 6) ) }>Next Anecdote</button>
<button onClick={ () => { setVotes(prevVotes => {
const upd = [...prevVotes];
upd[selected] += 1;
return upd;
})} }>Vote</button>
<p>votes: {votes[selected]}</p>
</div>
)
I think, using useReducer will help you keep everything in one place:
import React, { useState, useReducer } from "react";
const initialState = [
{ text: "If it hurts, do it more often", votes: 0 },
{
text: "Adding manpower to a late software project makes it later!",
votes: 0
},
{
text:
"The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.",
votes: 0
},
{
text:
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
votes: 0
},
{ text: "Premature optimization is the root of all evil.", votes: 0 },
{
text:
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.",
votes: 0
},
{
text:
"Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients",
votes: 0
}
];
const reducer = (state, action) => {
if (action.type === "VOTE_UP") {
return state.map((item, index) => {
if (index === action.index) {
item.votes = item.votes + 1;
}
return item;
});
}
};
const App = () => {
const [anecdotes, dispatch] = useReducer(reducer, initialState);
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div>
<h1>{anecdotes[selectedIndex].text}</h1>
<button
onClick={() => {
setSelectedIndex(Math.floor(Math.random() * 6));
}}
>
Next Anecdote
</button>
<button
onClick={() => {
dispatch({ type: "VOTE_UP", index: selectedIndex });
}}
>
Vote
</button>
<p>votes: {anecdotes[selectedIndex].votes}</p>
</div>
);
};
export default App;