React screen not updating after object update - javascript

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;
})

Related

Send a value to a base class from a child class and use it in a function

There are two problems, the first is that the value of select is pre-set, i.e. I change one input field and immediately select changes to other data (this is correct), but the value remains the same. (is displayed correctly - new data,
but keeps the previous one - old data)
The value that changes in child class must be sent to the base class and then used in the formatAmountFromLimit function, which is called and takes that value in the body of the lambda in base class
Base class
Fields:
function LimitsFields (props) {
const [unit, setUnit] = useState(props.limit.unit)
return <Form onSubmit={onSubmit}>
<Form.Group controlId='formGridLimitAmount' as={Col}>
<Form.Label>Limit Amount</Form.Label>
<Form.Control
type='text'
value={ limitAmount }
onChange={e => {
setLimitAmount(formatAmountFromLimit(e.currentTarget.value,unit))
}}/>
</Form.Group>
<Form.Group controlId='formGridUnit' as={Col}>
<Form.Label>Unit</Form.Label>
<UnitsDropdown
unit={unit}
setUnit={setUnit}
limitType={limitType}
entityType={entity}
siteGroup={siteGroup}
riskType={riskType}
isUpdate={isUpdate}/>
</Form.Group>
</Form.Row>
</Form >
}
export default LimitsFields
child class where change value (unit)
import React, {useState, useEffect} from 'react'
const UnitsDropdown = ({
unit,
setUnit,
limitType,
entityType,
riskType,
isUpdate,
}) => {
const [items, setItems] = useState([
{label: unit || 'Loading...', value: unit || ''}
])
useEffect(() => {
if (limitType === '') {
setItems([{label: 'All', value: ''}])
}
if (limitType && !isUpdate && entityType) {
getUnits()
}
if (isUpdate) {
getUnits()
}
}, [limitType, entityType])
const getUnits = async () => {
await fetch('api/units?limitType=' + limitType)
.then(res => res.json())
.then(body => {
setItems(body.map((name) => ({label: name, value: name})))
}).catch(e => {
console.log('UnitsDropdown Error' + e)
})
}
return (
<select className="form-control" disabled={isUpdate && riskType !== 'LENDING'} value={unit}
required={isUpdate && riskType !== 'LENDING'}
onChange={e => {
setUnit(e.currentTarget.value)
handleUnitChange(e)
}}>
{items.map(({label, value}) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
)
}
export default UnitsDropdown
function formatAmountFromLimit in base(Fields) where to apply the new value
<Form.Group controlId='formGridLimitAmount' as={Col}>
<Form.Label>Limit Amount</Form.Label>
<Form.Control
type='text'
value={ limitAmount }
onChange={e => {
setLimitAmount(formatAmountFromLimit(e.currentTarget.value,unit))
}}/>
</Form.Group>
If I understand correctly all you want to do is send a value from Child component to parent component right?
This is done using lifting state up.
https://beta.reactjs.org/learn/sharing-state-between-components
You can either declare a state in the parent component and pass its setState function to the child component.
Or you can pass the
formatAmountFromLimit function to the child component and call it from the child component itself when the value changes.

Material UI React - Autocomplete - How to reset the internal state

My goal is to reset the internal state of Autocomplete Material-UI's component.
My custom component is rendered N times in my cycle
{branches.map((branch, index) => {
return (
<BranchSetting
key={index}
index={index}
data={branch}
removeBranch={removeBranch}
/>
)
})}
branch is my hook state.
This is my removeBranch function:
const removeBranch = (index) => {
let listBranch = branches;
listBranch.splice(index, 1);
setBranches([...listBranch]);
}
Every time I delete an item to my array branch, everything works fine except the Autocomplete.
This is my BranchSetting component:
import React, { useState, useEffect } from "react";
import Autocomplete from '#material-ui/lab/Autocomplete';
const BranchSettings = ({ removeBranch, index, branchModify, data }) => {
const [ brands, setBrands ] = useState(data.brands);
const handleBrandSelected = (event, payload) => {
const values = payload.map(item => item.value);
setBrands(values);
}
useEffect(() => {
setBrands(data.brands);
}, [data])
return (
<>
<Autocomplete
id={'branch-brand'}
multiple
disableCloseOnSelect
options={carsBrand}
getOptionLabel={(carsBrand) => carsBrand.label}
onChange={handleBrandSelected}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option.value}
size="small"
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => {
return (
<TextField
{...params}
variant="filled"
fullWidth
label={'brand'}
/>
)
}}
/>
</>
)
}
export default BranchSettings
carsBrand it is my data source that in the example I avoided writing the population. It's an array
Everytime I try to delete an item, Autocomplete keep the state of the component ad the prev position.
I'm looking a way to reset all the internal state of Autocomplete component.
The status I refer to can be seen with the devToolBar
I'm looking a good way to keep the items selected properly or that every time the component has changed, rerender the Autocomplete component.
I resolved the problem.
The problem was that Autocomplete component need to input an array of objects with label and value keys.
In the function handleBrandSelected I saved into my brands status just the value. I should have saved the whole object because then it must be sent as input in Autocomplete with the props value.
And to handle the object I should have also used props getOptionSelected.
No problems with the remove function, and no problems with indexes. Only the values selected in inputs and compliant with the documentation were missing.
So this is the new code
import React, { useState, useEffect } from "react";
import Autocomplete from '#material-ui/lab/Autocomplete';
const BranchSettings = ({ removeBranch, index, branchModify, data }) => {
const [ brands, setBrands ] = useState(data.brands);
const handleBrandSelected = (event, payload) => setBrands(payload);
useEffect(() => {
setBrands(data.brands);
}, [data])
return (
<>
<Autocomplete
id={'branch-brand'}
multiple
disableCloseOnSelect
options={carsBrand}
getOptionLabel={(carsBrand) => carsBrand.label}
onChange={handleBrandSelected}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option.value}
size="small"
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => {
return (
<TextField
{...params}
variant="filled"
fullWidth
label={'brand'}
/>
)
}}
getOptionSelected={(option, value) => option.value === value.value}
value={brands}
/>
</>
)
}
export default BranchSettings
This problem is probably caused by using the array index as a key in <BranchSetting key={index}. I recommend that you add a unique id when creating the branch object, and use that id as a key instead. You can use performance.now() or a small lib like nanoid.
You can read more about the negative impacts of using an index as a key here.

Checkboxes in map not updating on array update after refactor to react hooks

I converted a class component into a function component using hooks. Currently, I'm struggling to figure out why the checkboxes within this map is not updating with checked value, despite the onChange handler firing, and updating the array as necessary. (The onSubmit also works, and updates the value within the DB properly).
import {
Container,
Typography,
Grid,
Checkbox,
FormControlLabel,
Button
} from "#material-ui/core";
import Select from "react-select";
import localeSelect from "../services/localeSelect";
import {
linkCharactersToGame,
characterLinked,
linkCharacters
} from "../data/locales";
import dbLocale from "../services/dbLocale";
import { LanguageContext } from "../contexts/LanguageContext";
import { UserContext } from "../contexts/UserContext";
import { GameContext } from "../contexts/GameContext";
import { CharacterContext } from "../contexts/CharacterContext";
import { Redirect } from "react-router-dom";
export default function LinkCharacter() {
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
const { games, loading, error, success, connectCharacters } = useContext(
GameContext
);
const { characters } = useContext(CharacterContext);
const [game, setGame] = useState("");
const [selectedCharacters, setSelectedCharacters] = useState([]);
if (!user) {
return <Redirect to="/" />;
}
return (
<section className="link-character">
<Container maxWidth="sm">
<Typography variant="h5">
{localeSelect(language, linkCharactersToGame)}
</Typography>
{error && (
<p className="error">
<span>Error:</span> {error}
</p>
)}
{success && <p>{localeSelect(language, characterLinked)}</p>}
<Select
options={games.map(game => {
return {
label: dbLocale(language, game),
value: game._id
};
})}
onChange={e => {
setGame(e.value);
const selected = [];
const index = games.findIndex(x => x._id === e.value);
games[index].characters.forEach(character => {
selected.push(character._id);
});
setSelectedCharacters(selected);
}}
/>
</Container>
<Container maxWidth="md">
{game !== "" && (
<>
<Grid container spacing={2}>
{characters.map((character, index) => {
return (
<Grid item key={index} md={3} sm={4} xs={6}>
<FormControlLabel
control={
<Checkbox
value={character._id}
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
selectedCharacters.push(e.target.value);
} else {
selectedCharacters.splice(index, 1);
}
}}
color="primary"
checked={
selectedCharacters.indexOf(character._id) !== -1
}
/>
}
label={dbLocale(language, character)}
/>
</Grid>
);
})}
</Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
connectCharacters(game, selectedCharacters);
}}
>
{localeSelect(language, linkCharacters)}
</Button>
</>
)}
</Container>
</section>
);
}
I feel like there's something I'm missing within Hooks (or there's some sort of issue with Hooks handling something like this). I have been searching and asking around and no one else has been able to figure out this issue as well.
The state returned by [state, setState] = useState([]) is something that you should only be reading from. If you modify it, React won't know that the data has changed and that it needs to re-render. When you need to modify data, you have to use setState, or in your case setSelectedCharacters.
Also, modifying the data by reference might lead to unpredictable results if the array is read elsewhere, later on.
In addition to that, if you give the same value to setState, that the hook returned you in state, React will skip the update entirely. It is not a problem when using numbers or strings, but it becomes one when you use arrays, because the reference (the value React uses to tell if there is a difference) can be the same, when the content might have changed. So you must pass a new array to setState.
With that in mind, your onChange function could look like:
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
// creating a new array with [], so the original one stays intact
setSelectedCharacters([...selectedCharacters, e.target.value]);
} else {
// Array.filter also creates new array
setSelectedCharacters(selectedCharacters.filter((char, i) => i !== index));
}
}}
Doc is here https://en.reactjs.org/docs/hooks-reference.html#usestate

Change input values from an object in React

I have multiple color inputs that are being displayed with unique colors. They are getting their value from a helper which is a nested Object. When I attempt to update the value, nothing occurs. I logged out the state and saw that all the colors are still an object. I attempted to get the individual color values and use that as the initial state with Object.values(), but there was no success in that.
As a test, I created a new input and state that held a random hex value and it updated without any issue. I'm assuming since I'm still getting back an object in my colorVal state, that I need to somehow get the values of the color object and convert it into a string?
I'm a bit lost and have been working on this for days now.
Component to display inputs
import React, { useState } from 'react'
import ColorPicker from './ColorPicker';
import { colorSelect, colorNames, colors } from '../Theme/colorSections'
import styled from 'styled-components';
function ColorPickerSection() {
const [colorVal, setColorVal] = useState(colors)
const onColorChange = (e) => {
setColorVal(e.target.value)
}
console.log(colorVal);
return (
<div>
{Object.keys(colorSelect).map(groupName => {
return (<div>
<GroupName>{groupName}</GroupName>
{Object.keys(colorSelect[groupName]).map(color => {
return (
<ColorPicker
key={color}
label={color}
value={colorVal[color]}
onChange={onColorChange}
/>
)
})}
</div>)
})}
</div>
)
}
Individual Color Swatch Component
import React from 'react';
import styled from 'styled-components'
function ColorPicker(props) {
return (
<ColorPickerContainer>
<p>{props.label}</p>
<ColorSwatch type="color" value={props.value} onChange={props.onColorChange} />
<HexInput
type="text"
value={props.value}
onChange={props.onColorChange}
/>
</ColorPickerContainer>
);
}
Color Helper
const colorSelect = {
'Line Highlights': {
highlightBackground: '#F7EBC6',
highlightAccent: '#F7D87C'
},
'Inline Code': {
inlineCodeColor: '#DB4C69',
inlineCodeBackground: '#F9F2F4'
},
'Code Blocks': {
blockBackground: '#F8F5EC',
baseColor: '#5C6E74',
selectedColor: '#b3d4fc'
},
'Tokens': {
commentColor: '#93A1A1',
punctuationColor: '#999999',
propertyColor: '#990055',
selectorColor: '#669900',
operatorColor: '#a67f59',
operatorBg: '#FFFFFF',
variableColor: '#ee9900',
functionColor: '#DD4A68',
keywordColor: '#0077aa'
}
}
const colorNames = []
const colors = {}
Object.keys(colorSelect).map(key => {
const group = colorSelect[key]
Object.keys(group).map(color => {
colorNames.push(color)
colors[color] = group[color]
})
})
export { colorSelect, colorNames, colors }
Assuming e.target.value is indeed the new color code in string.
The 'colors' is an object to begin with, so when you get the new color code, you need to update the object property accordingly
const onColorChange = (e, colorValKey) => {
setColorVal({
...colors,
[colorValKey]: e.target.value
})
}
return (
<div>
{Object.keys(colorSelect).map(groupName => {
return (<div>
<GroupName>{groupName}</GroupName>
{Object.keys(colorSelect[groupName]).map(color => {
return (
<ColorPicker
key={color}
label={color}
value={colorVal[color]}
onChange={(e) => onColorChange(e, color)}
/>
)
})}
</div>)
})}
</div>
)
Edit
// We passed onChange={onColorChange}, so props.onColorChange is undefined
<HexInput
type="text"
value={props.value}
onChange={props.onColorChange} />
// Should be
<HexInput
type="text"
value={props.value}
onChange={props.onChange} />

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

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.

Categories