How to rerender, if key stays the same, but other values change? - javascript

I'm writing a React app. I have a table of contacts:
// ... pure functional component that gets the contacts via props
return (
<Paper>
<table>
<thead>
<tr>
{fields.map(renderHeaderCell)}
</tr>
</thead>
<tbody>
{contacts.map(renderBodyRow)}
</tbody>
</table>
</Paper>
);
The renderBodyRow() function looks like this:
const renderBodyRow = contact => (
<ContactRow
key={contact.id}
contact={contact}
handleContactSave={handleContactSave}
/>
);
Now, when I update a contact and when the table isn't being sorted, the contact moves down the bottom of the list. But instead of rendering with the updated name, it renders with the old name. I assume this is because the contact.id key does not change. How can I get the row to render the new value?
For completeness sake (and because it could cause the problem), here is the ContactRow component. I don't think the problem is here thought
import PropTypes from 'prop-types';
import { equals, includes, map } from 'ramda';
import React, { useState } from 'react';
import { fields, groups, tendencies } from '../../config/constants';
import strings from './strings';
function ContactRow({ contact: original, handleContactSave }) {
const [contact, setContact] = useState(original);
const disabled = equals(contact, original);
const handleSaveButtonClick = () => {
handleContactSave(contact);
setContact(original)
};
const handeCancelButtonClick = () => {
setContact(original);
};
const renderOption = value => (
<option key={`${contact.id}-${value}`} value={value}>
{strings[value]}
</option>
);
const renderBodyCell = key => {
const value = contact[key];
const testId = `contact-${key}${
contact.id === 'new-contact' ? '-new-contact' : ''
}`;
const handleChange = e => {
e.preventDefault();
setContact({ ...contact, [key]: e.target.value });
};
return (
<td key={`${key}-${contact.id}`}>
{includes(value, [...groups, ...tendencies]) ? (
<select value={value} data-testid={testId} onChange={handleChange}>
{includes(value, groups)
? map(renderOption, groups)
: map(renderOption, tendencies)}
</select>
) : (
<input value={value} data-testid={testId} onChange={handleChange} />
)}
</td>
);
};
return (
<tr>
<td>
<button
aria-label={
contact.id === 'new-contact' ? 'create-contact' : 'update-contact'
}
onClick={handleSaveButtonClick}
disabled={disabled}
>
<span role="img" aria-label="save-icon">
💾
</span>
</button>
<button
aria-label={
contact.id === 'new-contact'
? 'cancel-create-contact'
: 'cancel-update-contact'
}
disabled={disabled}
onClick={handeCancelButtonClick}
>
<span role="img" aria-label="cancel-icon">
🔄
</span>
</button>
</td>
{map(renderBodyCell, fields)}
</tr>
);
}
ContactRow.propTypes = {
contact: PropTypes.shape({
/* fields */
}),
handleContactSave: PropTypes.func.isRequired
};
ContactRow.defaultProps = {
contact: fields.reduce((acc, field) => ({ ...acc, [field]: 'N/A' }), {}),
handleContactSave: () => {
console.warn('No handleContactSave() function provided to ContactRow.');
}
};
export default ContactRow;

Ok, so I see it now. The only prop you are passing to renderBodyCell is key, no other props. This is bad practice (and just wrong). keys are used as internal optimization hints to react and should not be used for props.
const renderBodyCell = key => {
const value = contact[key];
const testId = `contact-${key}${
contact.id === 'new-contact' ? '-new-contact' : ''
}`;
const handleChange = e => {
e.preventDefault();
setContact({ ...contact, [key]: e.target.value });
};
return (
<td key={`${key}-${contact.id}`}>
{includes(value, [...groups, ...tendencies]) ? (
<select value={value} data-testid={testId} onChange={handleChange}>
{includes(value, groups)
? map(renderOption, groups)
: map(renderOption, tendencies)}
</select>
) : (
<input value={value} data-testid={testId} onChange={handleChange} />
)}
</td>
);
};
Instead of passing in the key, you need to pass in the contact (or the contact and the key I guess, but I would hesitate to pass keys around as if they are meaningful unless you know exactly what you are doing).
EDIT:
So technically, you were correct, the row wasn't being re-rendering because the key didn't change, but that's because you were using it as a prop when you shouldn't have been.
EDIT #2:
Good time for you to go exploring about how React works. It is a very optimized machine. It doesn't just rerender components all the time, only when it needs to. In order to find out when it needs to rerender them, it checks props and state (or, in your case where you are doing this functionally, just the props - the function arguments) and compares them to the props the last time the component was rendered. If props are the same (shallow equals), then react just says screw it, I don't need to update, props are the same. At least that's the behaviour for PureComponent (which functional components are).
So if you want something to update, make sure the props you are passing it have changed.

Related

How can i check a value from a checkbox from another component in React

I have 3 components. The first component is where I created the checkbox and it looks like this.
const RoutingEnginesChecker = ({
classes,
routingEngines,
selectedEngines,
setSelectedEngines,
}) => {
// Add/Remove checked item from list
const handleCheck = (event) => {
let updatedList = [...selectedEngines];
if (event.target.checked) {
updatedList = [...selectedEngines, event.target.value];
} else {
updatedList.splice(selectedEngines.indexOf(event.target.value), 1);
}
setSelectedEngines(updatedList);
};
return (
<div className={classes.checkList}>
<Typography className={classes.routingEnginesTitle} variant='subtitle1'>
Routing Engines:
</Typography>
<div className={classes.routingEnginesContainer}>
{routingEngines.map((item, index) => (
<div key={index}>
<input value={item} type='checkbox' onChange={handleCheck} />
<span>{item}</span>
</div>
))}
</div>
</div>
);
};
export default withStyles(styles)(RoutingEnginesChecker);
After that I have the index component where i call the checkbox.
const routingEngines = ['here', 'tomtom'];
const [selectedEngines, setSelectedEngines] = useState([]);
<RoutingEnginesChecker
InputProps={{ label: 'setselectEngine Label' }}
routingEngines={routingEngines}
selectedEngines={selectedEngines}
setSelectedEngines={setSelectedEngines}
Finnaly, I have the third component where I need to check the values from the checkbox.
I have tried importing the component of the checkbox. This is how my code looks.
const routingeEngines = RoutingEnginesChecker;
// check what engine is used
if (engine === routingeEngines) {
return iconHere;
}
return iconTomTom
}
;
The problem is that it always goes on the first if('here' value). I have also tried using index and includes. How can I check for the 2 values(here and tomtom)? Thank you!

React screen not updating after object update

I have my object perfectly updated but it does not render on screen (filteredData / handleOrder function). I tried using callback but also do not render.
Does anyone know what is the possible solution to render the object on the screen (filteredData)?
Here is the code which the object is updated:
setFilteredData(filteredData.sort((a, b) => (
(sort === 'ASC')
? a[column].localeCompare(b[column])
: b[column].localeCompare(a[column])
)));
Full code:
import React, { useContext, useState } from 'react';
import { Button, Form } from 'react-bootstrap';
import MyContext from '../../context/MyContext';
import './style.css';
function FilterByNumericValues() {
const {
filteredData,
order,
functions: { setFilteredData, setOrder },
} = useContext(MyContext);
function handleOrder(event) {
event.preventDefault();
const { column, sort } = order;
setFilteredData(filteredData.sort((a, b) => (
(sort === 'ASC')
? a[column].localeCompare(b[column])
: b[column].localeCompare(a[column])
)));
console.log(filteredData, 'filtered');
}
return (
<Form>
<Form.Group className="mb-3" controlId="order">
<Form.Check
data-testid="column-sort-input-asc"
type="radio"
label="Ascendent"
name="orderOption"
value="ASC"
onChange={ () => setOrder({ ...order, sort: 'ASC' }) }
/>
<Form.Check
data-testid="column-sort-input-desc"
type="radio"
label="Descendent"
name="orderOption"
value="DESC"
onChange={ () => setOrder({ ...order, sort: 'DESC' }) }
/>
<Form.Label>Order By</Form.Label>
<Form.Select
data-testid="column-sort"
name="orderFilter"
onChange={ ({ target }) => setOrder({ ...order, column: target.value }) }
value={ order.column }
>
{[...columnsIn, ...columnsOut].map(({ name }, index) => (
<option key={ index } value={ name }>{ name }</option>
))}
</Form.Select>
<Button
className="filter__order"
data-testid="column-sort-button"
onClick={ (event) => handleOrder(event) }
type="submit"
variant="warning"
>
Sort
</Button>
</Form.Group>
</Form>
);
}
export default FilterByNumericValues;
Arrays are reference values, sort() won't change the reference (doesn't create a new array), so React bails out the state update (search the Docs about "bailing out of state update").
You should pass a sorted copy of your original array to setFilteredData():
[... originalArray].sort()
Moreover, since you new state depends on the previous state, you'd better use the syntax:
setState((previousState) => {
// transform previousState
return newState;
})

React - How to prevent re-rendering of all the input fields when input changes

I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.

How to avoid rerender all child components which in loop when parent component state update

I m having one child component which is inside a loop of parent component. when one of the child components is updating the state of parent component, it is re-rendering the all children since it is loop. How can i avoid the re-render for each iteration.
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = (event, id) => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if(checked){
if(!selectedChild.includes(id)){
updatedArray.push(id);
}
}
else{
var index = updatedArray.indexOf(id)
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
setSelectedChild(updatedArray);
}
const dummy = (id) => {
return selectedChild.includes(id);
}
return (
<div>
<table>
<tbody>
{[1,2,3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy}
/>
)
})}
</tbody>
</table>
<div>
{selectedChild}
</div>
</div>)
}
function Child({index, value, handle, isSelected }) {
console.log('rendering')
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected(index)}
onChange={(event) => handle(event, index)}/>
</td>
<td>hello {index} {value}</td>
</tr>
)
}
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
Current behaviour:
In above code, When i m clicking on the checkbox in one of the children component, it is updating the parent component state(selectedChild). So the loop is executing and all children(all table rows) are re rendering.
Expected behaviour:
Only that particular row have to go for re-render
Demo: https://codesandbox.io/s/newpro-0pezc
for that you can use React.memo that will memoize your component if props remains the same. But given your code you need to make some extra changes:
you have to apply useCallback to memoize onChangeHandle function;
to memoize properly onChangeHandle you need to refactor it. you can't pass selectedChild directly, otherwise it memoizes its value. use setSelectedChild passing as argument a function that takes selectedChild instead.
your Child should receive isSelected as boolean value instead of function. otherwise props will remain the same and Child never updates;
import React, { useState, memo, useCallback } from "react";
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = useCallback((event, id) => {
setSelectedChild(selectedChild => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if (checked) {
if (!selectedChild.includes(id)) {
updatedArray.push(id);
}
} else {
var index = updatedArray.indexOf(id);
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
return updatedArray;
});
}, []);
const dummy = id => {
return selectedChild.includes(id);
};
const renderChildren = () =>
[1, 2, 3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy(index)}
/>
);
});
return (
<div>
<table>
<tbody>{renderChildren()}</tbody>
</table>
<div>{selectedChild}</div>
</div>
);
}
const Child = memo(({ index, value, handle, isSelected }) => {
console.log("rendering");
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected}
onChange={event => handle(event, index)}
/>
</td>
<td>
hello {index} {value}
</td>
</tr>
);
});
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
https://stackblitz.com/edit/so-memo-children?file=src/App.js
The basic answer is use React.memo on Child.
const Child = memo(function Child(...) {...})
But to make memo work, the component needs to receive the same props if it shouldn't get rerendered. That means using useCallback on onChangeHandle:
const onChangeHandle = useCallback((event, id) => {...}, [])
But since onChangeHandle uses selectedChild that always changes on checkbox change, you'll also need to ref it using useRef:
const selectedChildRef = useRef();
selectedChildRef.current = selectedChild;
and use reffed version inside of onChangeHandle.
The last thing that needs to be done is to change isSelected prop from function to just a flag since it needs to be run on each checkbox change:
isSelected={selectedChild.includes(index)}
https://codesandbox.io/s/newpro-forked-wxvqs
You could implement shouldComponentUpdate (doc: https://reactjs.org/docs/react-component.html#shouldcomponentupdate) inside the definition of Child to have more control over when it rerenders. But that's only meant for cases where you have performance issues- generally you don't have to worry about it, and letting them all rerender is standard.

React JS map function

I have the following code and I'm trying to simplify it using a map function, perhaps on the array: const columns = ['Title', 'Author', 'Rating']
export const BookshelfListRow = (props) => {
return (
<tr className="table-row" >
<td>
<input onChange={(e) => { props.Update(e.target.value) }} placeholder={props.book.Title} />
</td>
<td>
<input onChange={(e) => { props.Update(props.book.Title, e.target.value) }} placeholder={props.book.Author} />
</td>
<td>
<input onChange={(e) => { props.Update(props.book.Title, props.book.Author, e.target.value) }} placeholder={props.book.Rating} />
</td>
</tr>
)}
Please note this is simplified - in my actual code I have 30 columns (meaning 30 separate inputs instead of 3) hence why I'm looking for a way to simplify it as it is currently really long - so essentially what is happening above is the placeholder is iterating through the array [Title,Author,Rating], and simultaneously on each new line we are adding an item from the array (in the form of props.book[item]) to the props.Update function. Any ideas how I could use a map function to carry this out?
You can use map to simplify it. The tricky bit will be the calling of Update with different number of parameters, but that too can be achieved using another map.
const columns = ['Title', 'Author', 'Rating'];
export const BookshelfListRow = (props) => {
return (
<tr className="table-row">
{
columns.map((column, i) => (
<td>
<input onChange={ e =>
props.Update(...[ // the parameters to Update consist of
...columns.slice(0, i).map(column => props.book[column]), // the column values from the start until the current column, map is used here to get the values for those columns
e.target.value // and the input value
])
}
placeholder={ props.book[column] } />
</td>
))
}
</tr>
)
}
Another approach:
The Update function is a mess. It can be a lot simpler if it just takes the column that was changed and the value as there is no need for it to send all those props back to the server if only one was changed, like so (this uses computed property names):
const Update = (column, value) => // takes the column that was changed and the value
axios.put('http://localhost:4001/books/update', { [column]: value }); // update only that column
Then the rendering will be much simpler also, like so:
const columns = ['Title', 'Author', 'Rating'];
export const BookshelfListRow = (props) => {
return (
<tr className="table-row">
{
columns.map((column, i) => (
<td>
<input onChange={ e => props.Update(column, e.target.value) } placeholder={ props.book[column] } />
</td>
))
}
</tr>
)
}
If you're using the keys of props.book, you can try something like this:
import React from "react";
const BookshelfListRow = props => {
const args = [];
return (
<tr className="table-row">
{Object.keys(props.book).map((key, idx) => {
if(idx > 0) {
args.unshift(key);
}
const argsCopy = [...args];
return (
<td>
<input
onChange={e => {
props.Update(...argsCopy, e.target.value);
}}
placeholder={props.book[key]}
/>
</td>
);
})}
</tr>
);
};
export default BookshelfListRow;
Otherwise, you can use an array like the one you suggested (const columns = ['Title', 'Author', 'Rating']) and take each value and add it to a copy with each map loop.
Mapping is a very powerful tool in React. It is most useful when you are try to DRY out some repeated code. In your case you are trying to DRY out your td's by mapping over the array columns.
Your columns array will need a little more info to make mapping useful. For instance,
const columns = ['Title', 'Author', 'Rating']
columns.map(column => console.log(column)) // Title, Author, Rating
That's not very helpful for your td because it needs the onChange, and placeholder and both require more information than just the strings 'Title', 'Author', and 'Rating'.
From what I can tell, your book prop is an object that looks something like this:
book: {
Title: 'some string',
Author: 'some other string',
Rating: 'some number maybe'
}
You can map over that by using Object.keys. But again, that only helps with the placeholder not the onChange.
The data that you have and the data you are trying to use for your inputs do not seem to have a common enough pattern to utilize map here.
Possible Solution
Modify your update function to not require so many parameters to keep the input field generic as possible that way you can map over your columns.
export const BookshelfListRow = (props) => {
// if you are using React without hooks, just replace this with
// normal state
const [state, setState] = useState({
title: '',
author: '',
rating: ''
})
const update = (e) => {
const input = e.currentTarget.value;
const attribute = e.currentTarget.name;
setState({...state, [attribute]: input})
}
const apiRequest = (e) => {
e.preventDefault();
// your state is now your request body
request(state)
}
const columns = ['Title', 'Author', 'Rating']
return (
<tr className="table-row" >
{columns.map(column => (
<td key={column}>
<input name={column.toLowerCase()} onChange={update} placeholder={column} />
</td>
))}
</tr>
)}
const columns = ['Title', 'Author', 'Rating']
const update = (val, column) => {
console.log(`${column}: ${val}`)
}
const BookshelfListRow = () => (<table><tbody><tr className="table-row">{
columns.map((column, i) => {
return (<td key={i}><input type="text" onChange = {e => update(e.target.value, column)} placeholder={column} /></td >)
})
}</tr></tbody></table>
)
ReactDOM.render(
<BookshelfListRow />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Categories