Understanding consequences of using bad react keys - reactjs

Recently, I made a trivial mistake using array values instead of indexes for keys like below:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
function NamesView(props: {names: string[], setNames(l: string[]) : void}) {
const {names, setNames} = props;
function setName(index: number, name: string) {
setNames(names.map((n, i) => i===index ? name : names[i]));
}
return <div>
{/***** THE key IS WRONG!!! */ }
{names.map((n, i) => <input key={n} value={n} onChange={e => setName(i, e.target.value)}/>)}
</div>;
}
function MainView() {
const [names, setNames] = useState(() => ['one', 'two']);
return <NamesView names={names} setNames={setNames}/>
}
ReactDOM.render(<MainView/>, document.getElementById('root'));
Here, using key={i} would be right. What I don't understand is the behavior of the wrong code: Each input can be edited just once. You can add or delete a character or paste something and it works, but all subsequent changes get ignored - until you e.g., change focus.
Can someone explain why?

When you are mapping through names which is an array of input values.
You are rendering the inputs in the following way.
return (
<div>
{/***** THE key IS WRONG!!! */}
{names.map((n, i) => (
<input key={n} value={n} onChange={(e) => setName(i, e.target.value)} />
))}
</div>
);
As soon as you change the input, setNames is triggered through your setName via onChange.
This means react will now check for new items generated.
Since key={n} would change the input itself i.e a new input element is generated and updated now in the dom, that is why you lose focus each time when you change it.
This is not the case, when you are using index as the key.
Because then the keys of input remain same and only the value changes.
However, there are some caveats using index as keys too.
You can read more about them here:
https://reactjs.org/docs/lists-and-keys.html
https://reactjs.org/docs/reconciliation.html#recursing-on-children

Keys are used for lists to help React identify which items have changed, been added or been removed. In your case you're using the name as the key, so whenever you update the value of your input field, the key is updated as well. React detects this as an element being removed, and a new element being added.
So instead of just updating the value, React actually removes the input field, and add an input field again, which is why the input loses focus after updating one of the inputs.

Related

Hello, on ReactJs, I loose focus on input when useState change value :

Here an example of the problem :
codesandbox.io
export default function App() {
const [hasInputChanged, setHasInputChanged] = useState(false);
let colorList = ["orange", "blue", "yellow"];
function handleChange(e) {
setHasInputChanged(true);
}
const MyLittleInput = () => {
return <input onChange={(e) => handleChange(e)} />;
};
return (
<>
{colorList.map((color) => (
<MyLittleInput key={color} />
))}
</>
);
}
I tried different solutions as defining Keys or using useRef but nothing worked
It's too much code to be debugged easily, but for what I can see on the fiddle, there are serveral things wrong, first of all you are doing really too much things for a simple increment/decrement of a input value. But most important you are defining theyr value using the parametresListe state, but never really changing it wit the setParametresListe function, which should be the only way to safely change controlled form inputs.
Just try to do a bit of cleaning on your code and to use the useState as it is meat to be used
Let us know any updates!
UPDATE:
Having a look at your cleaned code, the problem is that a input inside a component gets builded again and again.
The reason for that, is that each input should have they unique "key" prop, so react can easily understand what input is changed and update only that one.
You have 2 ways to make this work, for the first, I've edited your code:
import "./styles.css";
import React, { useState } from "react";
const DEFAULT_INPUT_STATE = {
orange: "",
blue: "",
yellow: ""
};
export default function App() {
let colorList = ["orange", "blue", "yellow"];
const [inputState, setInputState] = useState(DEFAULT_INPUT_STATE);
const handleChange = (e) => {
const { name, value } = e.target;
console.log(name);
setInputState({
...inputState,
[name]: value
});
};
return (
<>
{colorList.map((color, i) => (
<input
key={color}
name={color}
value={inputState[color]}
onChange={(e) => handleChange(e)}
/>
))}
</>
);
}
As you can see, I've just removed the component for the input and did a bit of other changes, but If you still want to use a component, you can moove all the .map function inside of it, but there's no way to create the input inside a component if it is in a .map function
There is too much code, difficult to follow through, in your example. In the nutshell, I see in dev tools, when I update an input, the entire example component is re-rendered, thus all input elements got destroyed and replaced by newly created ones, without focus. It must be just a bug in your code: once an input is updated it renders different stuff, instead of just changing the input value. But it is beyond something someone here would debug for you for free :D

Registering nested objects with React Hook Form

I have been working with RHF for a while and it helps a lot actually, but I have been trying to do something for what I have not enough knowledge maybe.
So the thing its that I have a nested object that I bring to my componen throw props
const Child = ({ globalObject, register }) => {
const renderNested = Object.entries(globalObject.nestedObject);
return (
<span>
{renderNested.map(([key, value], index) => {
return (
<div key={index}>
<Field
type="text"
label={key}
name{`nestedObject.${key}`}
defaultValue={value}
ref={register}
/>
</div>
);
})}
</span>
);
};
All good, now, one of the keys inside this nestedObject has another object as a value, for which when I map over them and display, the field will show: [object Object]
I know how I would solve that issue if I am using a useState, for example.
Not sure if its a good practice but I would go with something like:
defaultValue={typeof value === 'someKey' ? value[key] : value}
but in this case using register (which I want to use since it saved me from other nightmares) I am not sure how to solve this.
Should I filter the array outside and then render for one side the keys that dont have an object as a value and then the rest?
It looks to me like it has to be something better than that.
Can anyone give advices?
just to clarify, nestedObject looks like:
nestedObject: {
key: value
key: {key:value}
}
You can access the fields using . dot notation, as per documentation for the register method. So register("name.key") works to retrieve the nested object as well as arrays, however note that in React Hook Form v7 the syntax to retrieve nested array items changed from arrayName[0] to arrayName.0.
Your component would look similar to the following:
const Child = ({ globalObject, register }) => {
const nestedKeys = Object.keys(globalObject.nestedObject);
return (
<>
{nestedKeys.map((key) => (
<Field key={key} {...register(`nestedObject.${key}`)} />
))}
</>
);
};
You should not use index as key prop in this case, as you have another stable identifier for each array element which comes from unique nested object keys.

Setting keys for React components mapped from a string array with dynamic length

Problem
This is a question form, a question has many answers. User can add, edit, remove the answers in the form.
The answers is stored in a string array. We will use this array to render the answer input and its corresponding "remove" button.
What I've tried:
Set index as key: When remove an element from the array, React failed to render the remaining question (it removes the wrong element), although the values from useState computed correctly.
Set value as key: The input element which has the changing text re-rendered, thus it loses focus every time we type in a character.
How can we solve this?
CodeSandbox link: https://codesandbox.io/s/multiplechoicequestionform-2h0vp?file=/src/App.js
import { useState } from "react";
function MultipleChoiceQuestionForm() {
const [answers, setAnswers] = useState<string[]>([]);
const addAnswer = () => setAnswers([...answers, ""]); // Add a new empty one at bottom
const removeAnswerAtIndex = (targetIndex: number) => {
setAnswers(answers.filter((_, index) => index !== targetIndex));
};
const onAnswerChangeAtIndex = (newAnswer: string, targetIndex: number) => {
const newAnswers = [...answers];
newAnswers[targetIndex] = newAnswer;
setAnswers(newAnswers)
};
return <form>
{answers.map((answer, index) =>
<div
// I think the problem is the key, how to set this correctly ?
// Set to index: make removing elements has re-render errors
key={index}
// key={answer} // Lose focus on each character typed
style={{ display: "flex" }}
>
<input type="text" onChange={(e) => onAnswerChangeAtIndex(e.target.value, index)} />
<button onClick={(_) => removeAnswerAtIndex(index)}>Remove</button>
</div>
)}
<button onClick={addAnswer}>Add</button>
</form>
}
I think you forgot to bind your answer values to the input element. Right now all your inputs are uncontrolled which is not the best way to handle dynamic forms where you add or delete items, better make them controlled.
Just do that:
<input
type="text"
value={answer}
onChange={(e) => onAnswerChangeAtIndex(e.target.value, index)}
/>
Working Codesandbox example
Other good practice here might be using some other structure for answers, for example, instead of string create an object with id (you can generate them by yourself, as an easiest way just use Math.random()) and value property for each answer, that way you could use that id as real key.

React state showing differing values from each dynamically generated input field component

I've been banging my head against my keyboard for the past 8 hours trying to figure out why each input field is showing different values for the same state.
Here is the code -
const BeaconSettingsCard = (props) => {
const [settingsItems, setSettingsItems] = useState([]);
const handleAddBeaconBtnOnClick = () => {
const id = settingsItems.length;
const newItem = (
<InputItem
id={id}
key={id}
type="InputField"
title="Test Title"
value="Test Value"
onChange={(e) => handleBeaconIdInputFieldOnChange(e, id)}
/>
);
setSettingsItems((settingsItems) => [...settingsItems, newItem]);
};
const handleBeaconIdInputFieldOnChange = (e, id) => {
console.log("settingsItems: ", settingsItems); // each input field shows a different settingsItems value ??
};
let cardHeaderButton = (
<InputItem type="Button" width="150px" onClick={handleAddBeaconBtnOnClick}>
Click to Add
</InputItem>
);
return (
<SettingsCard
headerButton={cardHeaderButton}
settingsItems={settingsItems}
/>
);
};
export default BeaconSettingsCard;
When I log the "settingsItems" state in the onChange event for each input field, I get different values.
On the first dynamically generated inputfield, it logs settingsItems as []. On the second, it logs [{React Component}]. On the third, it logs [{React Component}, {React Component}] and so forth.
These should all be logging the same state value! My friend who is a react wiz couldn't seem to figure this out either. Really hoping someone here can. Thank you.
I solved this strange issue by converting the code from React hooks to a normal React component.
Not ideal, but works. Hopefully if someone runs into a strange issue like this, this solution will work for you too.

Odd behaviour from custom react hook

https://jsfiddle.net/dqx7kb91/1/
In this fiddle, I extracted a part of a project (I tried to simplify it as much as I could without breaking anything), used to manage search filters.
We have many different types of filters defined as an object like this:
filter = {
name: "",
initVal: 1, //default value for the filter,
controlChip: ... // filter's chip component
...
}
the chip component are used to list all filters activated and to edit already activated filters (in this case remove the filter from the list of activated filters).
The filters are contained in an object handled by an unique container containing a custom hook.
The problem is, let's say I set the 1st filter, then set the second and I decide to finally remove the first filter, both filters are removed. Everything is working fine apart from this.
What might be causing this ? It seems to me that in the custom hook useMap that I use, when I try to remove the filter, it doesn't take account of the actual current state but uses the state it was when I added the first filter (an empty object), so it tries to remove something from nothing and set the state to the result, an empty object.
How can I fix this ? Thank you
What's happening is when you set your first filter (let's say #1) you're setting map that contains just filter 1. When you set filter #2 map contains filters #1 & #2. BUT... and here's the thing... your remove callback for filter #1 has map only containing #1, not #2. That's because your callback was set before you set filter #2. This would normally be solved because you're using hooks like useCallback, but the way you're implementing them (createContainer(useFilters)), you're creating separate hooks for each filter object.
I would simplify this into only one component and once it is working start extracting pieces one by one if you really need to.
I know this is a complete rewrite from what you had, but this works:
import React from "react";
import ReactDOM from "react-dom";
const App = () => {
const [map, setMap] = React.useState({});
// const get = React.useCallback(key => map[key], [map])
const set = (key, entry) => {
setMap({ ...map, [key]: entry });
};
const remove = key => {
const {[key]: omit, ...rest} = map;
setMap(rest);
};
// const reset = () => setMap({});
const filters = [
{ name: 'filter1', value: 1 },
{ name: 'filter2', value: 2 },
];
return (
<>
{Object.keys(map).map(key => (
<button onClick={() => remove(key)}>
REMOVE {key}
</button>
))}
<hr />
{filters.map(({ name, value }) => (
<button onClick={() => set(name, { value })}>
SET {name}
</button>
))}
</>
)
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Resources