How do I change class names for dynamic children? - reactjs

I want to have the email address validate onBlur and change the class to invalid, so the guest knows. How do I do that with dynamic children? Also, this is my first time playing with React, so feel free suggest better conventions.
https://jsfiddle.net/reactjs/69z2wepo/
var EmailList = React.createClass({
getInitialState: function() {
return { emailInputs: [this.createInput()] };
},
validateEmail: function(event) {
var email = event.target.value;
var re = /^([\w-]+(?:\.[\w-]+)*)#((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
return re.test(email);
},
createInput: function() {
return <li><input class="email" onBlur={this.validateEmail} type="text" placeholder="Email Address"/></li>
},
getEmailInputs: function() {
if (this.state.emailInputs.length < this.props.quantity) {
// Add inputs
for(var x = this.state.emailInputs.length; x < this.props.quantity; x++) {
this.state.emailInputs.push(this.createInput())
}
} else {
// Remove inputs
for(var x = this.state.emailInputs.length; x > this.props.quantity; x--) {
this.state.emailInputs.pop()
}
}
return this.state.emailInputs;
},
render: function() {
return <ol>
{this.getEmailInputs()}
</ol>
}
});
React.render(<EmailList quantity="5" />, document.getElementById('container'));

First of all "NEVER mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable."
https://facebook.github.io/react/docs/component-api.html
Use this.setState({myarray:[email]}); instead. Loop and add the complete array to the state with setState then render {this.state.myarray}
In fact I would put the quantity in the state and add the corresponding number of div to an array and render this array (without stocking this array in the state, just the quantity prop).
To answer your initial question I would just change the class using jquery(event.target)... because I'm lazy. But I suspect using the state to store the state ;) would be cleaner. (An array of the state of each div binded with the keys, btw react will probably ask for keys)
I hope it helps

The React convention is to use render() to generate content which is a reflection of your component's current state. As such, the bits which can change are usually modelled as state which is either passed in as props or stored in the component's own state.
In this case you need some state for each each email input, so at a minimum you'll need value and valid state for each one. Since there are a list of inputs, the state could be a list of objects with value and valid properties:
var DEFAULT_FIELD = {name: '', valid: null}
// ...
getInitialState() {
return {
fields: Array(this.props.quantity).fill(DEFAULT_FIELD)
}
},
With this state, you could render the fields something like this:
render() {
return <ol>
{this.state.fields.map((field, index) => <li>
<input className={'email' + field.valid === false ? ' invalid' : ''}
key={index}
value={field.value}
placeholder="Email Address" />
</li>)}
</ol>
}
This will be read-only because you haven't provided a way for input to update the state. You need to know which index the edited field corresponds to in the array in your state:
handleChange(index, event) {
// Update this.state.fields[index] with new value
},
// ...
render() {
return <ol>
{this.state.fields.map((field, index) => <li>
<input className={'email' + field.valid === false ? ' invalid' : ''}
key={index}
value={field.value}
placeholder="Email Address"
onChange={this.handleChange.bind(this, index) />
</li>)}
</ol>
}
You'll also need the index for onBlur validation:
handleBlur(index, event) {
// Update this.state.fields[index] with new validation status
},
// ...
render() {
return <ol>
{this.state.fields.map((field, index) => <li>
<input className={'email' + field.valid === false ? ' invalid' : ''}
key={index}
value={field.value}
placeholder="Email Address"
onChange={this.handleChange.bind(this, index)
onBlur={this.handleChange.bind(this, index) />
</li>)}
</ol>
}
I have a runnable full implementation of this which uses React's update() immutability helper to avoid mutating the state:
https://gist.github.com/insin/407de6bb93dfe077433f
If you download and extract this Gist and run npm install, running npm start will start a webpack dev server on http://localhost:3000 which supports hot reloading so you can play about with it - e.g. tweak the quantity prop being passed from App.js.

Related

React hooks - cannot type in input field [NOT duplicate]

You can check the full code here:
CODESANDBOX LINK
This is really weird.
I have a list of input fields which are rendered dynamically depending on the button click
const newFields = (index) =>
<div>
<input value={inputs[index].name} onChange={e => handleNameInput(e,index)} />
<input value={inputs[index].age} type="number" onChange={e => handleAgeInput(e,index)} />
<Button onClick={() => setInputArray(inputArray => [...inputArray, newFields(++index)])}>Add</Button>
</div>
I'm trying to store the values of the inputs and update them onChange. But as stated in the title, I can't type in the input field and once I click a character the input loses focus. I don't know why is that happening.
Here are the states and the handleInput function:
let [inputs, setInputs] = useState(Array.apply(null, Array(100)).map(function () { return {
name: '',
age: ''
} }));
const handleNameInput = (e, i) =>{
let tempArr = new Array();
tempArr = [...inputs];
tempArr[i].name = e.target.value;
setInputs([...tempArr]);
}
//handleAgeInput is exactly the same
I think your main issue is the duplicate usage of arrays. The facts is, you want only one array of datas. So, you need to build your component around it. If you make another array on state, you're adding complexity and it's resulting to you're current issue.
Understand why there is no updates
You're onChange function call update the inputs array state. BUT, you're rendering the inputArray where nothing change to him. No setInputArray call so, no re-render.
Proposal
There is proposal implementation of what I understand of you needs :
function App() {
let [inputArray, setInputArray] = useState([{
name: "",
age: ""
}]);
const handleNameInput = useCallback(
(e, i) => {
inputArray[i].name = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleAgeInput = useCallback(
(e, i) => {
inputArray[i].age = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleNewField = useCallback(() =>
setInputArray(inputArray => [...inputArray, {
name: "",
age: ""
}]),
[]
);
return (
<div className="App">
{inputArray.map((element, i) => (
<div>
<input
value={element.name}
onChange={e => handleNameInput(e, i)}
/>
<input
value={element.age}
type="number"
onChange={e => handleAgeInput(e, i)}
/>
<button
onClick={handleNewField}>
Add
</button>
</div>
))}
</div>
);
}
Depending of you needs maybe you should adapt it. There is some ways to avoid re-render the whole list. But this way you can edit all fields
One another proper way to do this is to to make each fields group as another Component.with independents states.
If you want the button juste once, move him from the loop.
If you want to display only the last fields set, take the inputArrays[inputArray.length] instead of loop on all.
Explanation :
You're rendering an array of components { inputArray.map((e, i) => { return <div key={i}> {e} </div>; })} where e should be a component.
In another hand, you're maintaining an array of datas that's "shadowing" the first array.
The main concept of your use case as far I can understand, should be to render your components directly from your datas – your array of datas.
This way you have only one source of truth.

Splice is removing the last element of array not index - React

I want each note to have a delete button that when clicked removes that note from an array in my state but .splice is removing the last element not the index.
I added an alert statement to verify index is correct. The alert says the correct number but the splice removes the last element. Why is this happening?
constructor(props) {
super(props);
this.state = {
Notes: ["1","2","3","4"]
}
}
addNote = () => {
var noteList = [...this.state.Notes];
var newNote = "";
this.setState({ Notes: noteList.concat(newNote) });
}
deleteNote = (index) => {
var noteList = [...this.state.Notes];
alert(index);
noteList.splice(index, 1);
this.setState({ Notes: noteList });
}
renderNotes(Notes) {
return (
<div>
{Notes.map((Note, index) =>
<div class="note">
<div class="noteTop">
<button id="menu"><FontAwesomeIcon icon={faEllipsisV} /></button>
<button id="delete" onClick={() => this.deleteNote(index)}><FontAwesomeIcon icon={faTimes} /></button>
</div>
<textarea class="noteMain">{Note}</textarea>
</div>
)}
</div>
);
}
A couple of things at issue here.
First, you're rendering an array of elements that you modify but aren't using a key on the divs you're rendering (see: https://reactjs.org/docs/lists-and-keys.html#keys). Your console should show a warning "Warning: Each child in a list should have a unique "key" prop.". If you add a key to the div, it'll render correctly with your code as-is. The note itself makes for a unique key in your example although you may want to change it later to something that doesn't assume unique note values. Codepen: https://codesandbox.io/s/eager-cray-41dv7
<div className="note" key={Note}>
Second, but not as important after the first fix but will affect you later when you want to let people update the note: React doesn't support children in <textarea> elements. See https://reactjs.org/docs/forms.html#the-textarea-tag.
If you change your text area code the below, you'll see the updates reflected correctly in the UI and it'll later be useful when you enable editing.
<textarea className="noteMain" value={Note} />
Modify your deleteNote method to this:
deleteNote = (index) => {
var noteList = this.state.Notes;
alert(index);
this.setState({ Notes:[...noteList.slice(0, index), ...noteList.slice(index + 1)] });
}
Here's a working example: https://codesandbox.io/s/serene-albattani-gymrx

Having issues setting array state of child component and passing it back to parent in a react quiz

I'm a student and for my project one of my functions is a quiz. I am getting the questions from my backend, doing a componentDidMount inside QuizContainer, then passing each question down to QuizForm as props. Inside QuizForm I have 5 radio button for each question which represent a range from strongly disagree to strongly agree. When I set setState inside the child (QuizForm) component, it only registers the last onClick event and only sends that up to the parent. I think it might be rendering each question as it's own QuizForm, but I'm not sure how to gather all of that data inside the parent (Maybe as a state of the parent, but I'm not sure how I'd go about that).
Here's the code for reference:
QuizContainer
state = {
all: []
}
handleSubmit = (evt, quizObj) => {
evt.preventDefault()
this.setState = {
all: [...this.state.all, quizObj]
}
}
render() {
return (
<container className="quiz">
<h5>For each of the following statements choose on a scale of 1 - 5, one strongly disagree and 5 being strongly agree, and 3 being neutral. </h5>
<div> {this.props.questions.questions.map(question => <QuizForm question={question} key={question.id} handleSubmit={this.handleSubmit}/>)}
<input form="quiz-form" type="submit" value="Submit" />
</div>
</container>
)
}
QuizForm
state = {
question: [],
answer: []
}
handleChange = (evt) => {
let name = evt.target.name
let value = evt.target.value
this.setState({
questions: [...this.state.questions, name],
answers: [...this.state.answers, value]
})
}
render() {
return (
<form id="quiz-form" onSubmit={(evt) => this.props.handleSubmit(evt, this.state)}>
<label htmlFor="question">{this.props.question.question}</label>
<input onChange={this.handleChange}
type="radio"
name={this.props.question.id}
value={0}
/>
<input onChange={this.handleChange}
type="radio"
name={this.props.question.id}
value={25}
/>
</form>
)
}
You are using map() method, which basically loops array, so yes, it is rendering each question as different form. You shoulnt put every question as different form, just wrap them all into one and remove tag from QuizForm.
Also handleSubmit should be method that submits form to backend (or w.e.), and create another method like handleChange or something, that accepts argument and sets state.
handleChange(questionName, selectedAnswer){
this.setState({...answers, {questionName, selected: selectedAnswer}})
}
this way you can keep name of question (id or something like that would be better) and selected answer and pass it as props to your QuizForm (which also needs renaming to something like Question) and check the radio if it maches selectedAnswer prop.
Your current QuizForm component should look something like that (which also should be functional component, not class)
const radioButtons = [{id: 'first', value: 1}, id:'second', value: 2];
function Question({selectedAnswer, question, name, id}){
return (
<>
<label htmlFor={id}>{question}</label>
{radioButtons.map(({id: btnId, value}) =>
{<input type="radio" id={id.concat(btnId)}
{selectedAnswer === value ? checked : null}
name={name} value={value} />})}
</>
);
}
In this example you have to pass question id, name, selectedAnswer and question to display everything correct. Id will be used for label, since it refers to element id, which has to be unique in every page.

Unable to set state in a react component

I have a react component that looks like this. I call a method cleanUpInvoices
to format a date in my array object(invoices). This works very well without any
problem. I tried to setState to dateCleanUpResult.
All I get is "dateCleanUpResult is not defined". I have tried so many things and nothing works.
I am not able to set state.
What is wrong with this code?
Here is the entire code
class Tester extends PureComponent {
constructor(){
super();
this.state = {
invoices:[],
startDate:'',
endDate:'',
queryResult:[],
dateCleanUpResult:[]
};
this.searchForInvoicesByDates = this.searchForInvoicesByDates.bind(this);
this.handleChange = this.handleChange.bind(this);
this.cleanUpInvoices = this.cleanUpInvoices.bind(this);
}
handleChange({ target }) {
this.setState({
[target.name]: target.value
});
}
componentDidMount() {
const getCustomerId = this.props.customer.customerId;
AXIOS_AUTHED.get(`${API}/customers/${getCustomerId}/invoices?sort=settledDate,desc`)
.then(res => {
const invoices= res.data.content;
this.setState({ invoices });
})
}
cleanUpInvoices(){
const invoice = this.state.invoices;
invoice.forEach(function(invoicer) {
const newDate = invoicer.settledDate.substring(0, invoicer.settledDate.indexOf('T'));
invoicer.settledDate = moment(newDate, 'YYYY-MM-DD').format('MM-DD-YYYY');
});
return this.setState({
dateCleanUpResult: invoice
}, () => this.state.dateCleanUpResult);
}
searchForInvoicesByDates(startDate, endDate){
var myResult = this.cleanUpInvoices();
console.log(myResult);
//now perform your date search based on the result from above
let cleanedStartDate = moment(startDate).format('MM-DD-YYYY');
let cleanedEndDate = moment(endDate).format('MM-DD-YYYY');
let filteredResult = [];
for(let i = 0; i < this.state.dateCleanUpResult.length; i++){
if(this.state.dateCleanUpResult[i].settledDate >= cleanedStartDate && this.state.dateCleanUpResult[i].settledDate <= cleanedEndDate) {
filteredResult.push(this.state.dateCleanUpResult[i]);
}
}
console.log(filteredResult);
const listItems = filteredResult.map((number) =>
<li key={number.orderNumber}>{number.orderNumber} - {moment(number.settledDate).format('MMM-DD-YYYY')} </li>
);
this.setState({queryResult:listItems});
return (
<ul>{listItems}</ul>
);
}
render() {
return (
<PageBase
navigation={['Customer Solution', 'Tester App']}
>
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<Paper>
<Typography className="customer-solution-subheader" component="h3" variant="subheading">
Tester App
</Typography>
<form>
<div className="customer-form-details">
<span>DATE RANGE COVERED*</span><br/>
<span className="ctrCalendar">
<label htmlFor="start">Start date:</label>
<input type="date" id="start" name="startDate" value={this.state.startDate} onChange={this.handleChange} required></input>
</span>
<span className="ctrCalendar">
<label htmlFor="start">End date:</label>
<input type="date" id="end" name="endDate" value={this.state.endDate} onChange={this.handleChange} required></input>
</span>
<span>
<Button variant="contained" className="next-button" id="btnSearchDates" onClick={() =>this.searchForInvoicesByDates(this.state.startDate, this.state.endDate)}>Search</Button><br/><br/>
</span>
<p>Search Result (Invoices/Dates)</p>
<div role="content" className="invContentParent">
<div name="teach" id="invContentChild">
</div>
</div>
</div>
</form>
</Paper>
</div>
</div>
</PageBase>
);
}
}
export default Tester;
It's almost right, but what's happining is that when you return the for the array it could be just the empty array you are initializing at the state of the component.
We fix that be passing a callback function to setState method, which is a function that returns the state you want, the updated one right?
That callback function will be invoked after insuring that the new state has been set, and, we also return the setState function, because it's the one that is returning the new state.
return this.setState({
dateCleanUpResult: invoice
}, () => this.state.dateCleanUpResult);
That ARTICLE is a good explanation for that matter.
I created a jsfiddle showing that you can update the state in your case.
http://jsfiddle.net/efp82rjg/3/
I think you are getting an issue because you are assuming cleanUpInvoices() will return the updated value of state. But it won't because setState is asynchronous and even though the value will be updated but it won't show the updated value to you. If you want to access the updated value after setState() then use the call back that is available to you after setState function. Please read the documentation here: https://reactjs.org/docs/react-component.html#setstate
class Hello extends React.Component {
constructor() {
super();
this.state = {
invoices: [
{ settledDate: 'no' },
{ settledDate: 'no' },
],
dateCleanUpResult: [],
};
this.cleanUpInvoices = this.cleanUpInvoices.bind(this);
}
cleanUpInvoices() {
const invoice = this.state.invoices;
invoice.forEach((invoicer) => {
invoicer.settledDate = 'testing';
});
this.setState({
dateCleanUpResult: invoice,
});
return this.state.dateCleanUpResult;
}
render() {
return (
<div>
<button onClick={this.cleanUpInvoices}>test</button>
{this.state.dateCleanUpResult.map(item => (
<div>{item.settledDate}</div>
))}
</div>
);
}
I'm assuming you receive the value of "undefined" where you are logging the returned value cleanUpInvoices in your searchForInvoicesByDateMethod. Although this is the result of the async nature of setState, implementing a setState callback will not remedy this issue (without more, it will merely let you access the updated state in the scope of that handler). If you want to stick to your current implementation, I would return a promise and async/await the returned promise value. This will delay execution of code in your searchForInvoicesByDates method when cleanUpInvoices is called.
cleanUpInvoices(){
// Avoid directly mutating state and generating circular reference by creating new array with map
const invoice = this.state.invoices.map(invoice => Object.assign({}, invoice));
//.....
//.....
return new Promise((res) => {
return this.setState({
dateCleanUpResult: invoice
}, () => res(this.state.dateCleanUpResult)));
}
}
////////
async searchForInvoicesByDates(startDate, endDate){
var myResult = await this.cleanUpInvoices();
//....
}
Trying not to deviate from the question posed, here is another quick thought that might simplify the implementation. Looks like you're initially making an API call on componentDidMount to retrieve the full universe of invoices, then generating a subset by date based on user input. Is it necessary to maintain the generated subset in state? Each time invoices are searched for by date, it seems you want to use the full universe returned from the API as your filter starting point. If that's the case, you can just immediately return the result from cleanUpInvoices -- no need for a state update. This would be different, of course, if you needed the invoice subset for further reference or manipulation.

How to create unique keys for React elements?

I am making a React app that allows you to make a list and save it, but React has been giving me a warning that my elements don't have a unique key prop (elements List/ListForm). How should I create a unique key prop for user created elements? Below is my React code
var TitleForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var listName = {'name':this.refs.listName.value};
this.props.handleCreate(listName);
this.refs.listName.value = "";
},
render: function() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input className='form-control list-input' type='text' ref='listName' placeholder="List Name"/>
<br/>
<button className="btn btn-primary" type="submit">Create</button>
</form>
</div>
);
}
});
var ListForm = React.createClass({
getInitialState: function() {
return {items:[{'name':'item1'}],itemCount:1};
},
handleSubmit: function(e) {
e.preventDefault();
var list = {'name': this.props.name, 'data':[]};
var items = this.state.items;
for (var i = 1; i < items.length; i++) {
list.data.push(this.refs[items[i].name]);
}
this.props.update(list);
$('#'+this.props.name).remove();
},
handleClick: function() {
this.setState({
items: this.state.items.concat({'name':'item'+this.state.itemCount+1}),
itemCount: this.state.itemCount+1
});
},
handleDelete: function() {
this.setState({
itemCount: this.state.itemCount-1
});
},
render: function() {
var listItems = this.state.items.map(function(item) {
return (
<div>
<input type="text" className="list-form" placeholder="List Item" ref={item.name}/>
<br/>
</div>
);
});
return (
<div>
<form onSubmit={this.handleSubmit} className="well list-form-container">
{listItems}
<br/>
<div onClick={this.handleClick} className="btn btn-primary list-button">Add</div>
<div onClick={this.handleDelete} className="btn btn-primary list-button">Delete</div>
<button type="submit" className="btn btn-primary list-button">Save</button>
</form>
</div>
)
}
});
var List = React.createClass({
getInitialState: function() {
return {lists:[], savedLists: []};
},
handleCreate: function(listName) {
this.setState({
lists: this.state.lists.concat(listName)
});
},
updateSaved: function(list) {
this.setState({
savedLists: this.state.savedLists.concat(list)
});
},
render: function() {
var lst = this;
var lists = this.state.lists.map(function(list) {
return(
<div>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
var savedLists = this.state.savedLists.map(function(list) {
var list_data = list.data;
list_data.map(function(data) {
return (
<li>{data}</li>
)
});
return(
<div>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
var save_msg;
if(savedLists.length == 0){
save_msg = 'No Saved Lists';
}else{
save_msg = 'Saved Lists';
}
return (
<div>
<TitleForm handleCreate={this.handleCreate} />
{lists}
<h2>{save_msg}</h2>
{savedLists}
</div>
)
}
});
ReactDOM.render(<List/>,document.getElementById('app'));
My HTML:
<div class="container">
<h1>Title</h1>
<div id="app" class="center"></div>
</div>
There are many ways in which you can create unique keys, the simplest method is to use the index when iterating arrays.
Example
var lists = this.state.lists.map(function(list, index) {
return(
<div key={index}>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
Wherever you're lopping over data, here this.state.lists.map, you can pass second parameter function(list, index) to the callback as well and that will be its index value and it will be unique for all the items in the array.
And then you can use it like
<div key={index}>
You can do the same here as well
var savedLists = this.state.savedLists.map(function(list, index) {
var list_data = list.data;
list_data.map(function(data, index) {
return (
<li key={index}>{data}</li>
)
});
return(
<div key={index}>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
Edit
However, As pointed by the user Martin Dawson in the comment below, This is not always ideal.
So whats the solution then?
Many
You can create a function to generate unique keys/ids/numbers/strings and use that
You can make use of existing npm packages like uuid, uniqid, etc
You can also generate random number like new Date().getTime(); and prefix it with something from the item you're iterating to guarantee its uniqueness
Lastly, I recommend using the unique ID you get from the database, If you get it.
Example:
const generateKey = (pre) => {
return `${ pre }_${ new Date().getTime() }`;
}
const savedLists = this.state.savedLists.map( list => {
const list_data = list.data.map( data => <li key={ generateKey(data) }>{ data }</li> );
return(
<div key={ generateKey(list.name) }>
<h2>{ list.name }</h2>
<ul>
{ list_data }
</ul>
</div>
)
});
It is important to remember that React expects STABLE keys, meaning you should assign the keys once and every item on your list should receive the same key every time, that way React can optimize around your data changes when it is reconciling the virtual DOM and decides which components need to re-render.
So, if you are using UUID you need to do it at the data level, not at the UI level.
Also keep in mind you can use any string you want for the key, so you can often combine several fields into one unique ID, something like ${username}_${timestamp} can be a fine unique key for a line in a chat, for example.
Keys helps React identify which items have changed/added/removed and should be given to the elements inside the array to give the elements a stable identity.
With that in mind, there are basically three different strategies as described bellow:
Static Elements (when you don't need to keep html state (focus, cursor position, etc)
Editable and sortable elements
Editable but not sortable elements
As React Documentation explains, we need to give stable identity to the elements and because of that, carefully choose the strategy that best suits your needs:
STATIC ELEMENTS
As we can see also in React Documentation, is not recommended the use of index for keys "if the order of items may change. This can negatively impact performance and may cause issues with component state".
In case of static elements like tables, lists, etc, I recommend using a tool called shortid.
1) Install the package using NPM/YARN:
npm install shortid --save
2) Import in the class file you want to use it:
import shortid from 'shortid';
2) The command to generate a new id is shortid.generate().
3) Example:
renderDropdownItems = (): React.ReactNode => {
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={shortid.generate()}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
IMPORTANT: As React Virtual DOM relies on the key, with shortid every time the element is re-rendered a new key will be created and the element will loose it's html state like focus or cursor position. Consider this when deciding how the key will be generated as the strategy above can be useful only when you are building elements that won't have their values changed like lists or read only fields.
EDITABLE (sortable) FIELDS
If the element is sortable and you have a unique ID of the item, combine it with some extra string (in case you need to have the same information twice in a page). This is the most recommended scenario.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${item.id}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
EDITABLE (non sortable) FIELDS (e.g. INPUT ELEMENTS)
As a last resort, for editable (but non sortable) fields like input, you can use some the index with some starting text as element key cannot be duplicated.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach((item:any index:number) => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${index}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
Hope this helps.
Do not use this return `${ pre }_${ new Date().getTime()}`;. It's better to have the array index instead of that because, even though it's not ideal, that way you will at least get some consistency among the list components, with the new Date function you will get constant inconsistency. That means every new iteration of the function will lead to a new truly unique key.
The unique key doesn't mean that it needs to be globally unique, it means that it needs to be unique in the context of the component, so it doesn't run useless re-renders all the time. You won't feel the problem associated with new Date initially, but you will feel it, for example, if you need to get back to the already rendered list and React starts getting all confused because it doesn't know which component changed and which didn't, resulting in memory leaks, because, you guessed it, according to your Date key, every component changed.
Now to my answer. Let's say you are rendering a list of YouTube videos. Use the video id (arqTu9Ay4Ig) as a unique ID. That way, if that ID doesn't change, the component will stay the same, but if it does, React will recognize that it's a new Video and change it accordingly.
It doesn't have to be that strict, the little more relaxed variant is to use the title, like Erez Hochman already pointed out, or a combination of the attributes of the component (title plus category), so you can tell React to check if they have changed or not.
edited some unimportant stuff
Let React Assign Keys To Children
You may leverage React.Children API:
const { Children } = React;
const DATA = [
'foo',
'bar',
'baz',
];
const MyComponent = () => (
<div>
{Children.toArray(DATA.map(data => <p>{data}</p>))}
</div>
);
ReactDOM.render(<MyComponent />,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>
To add the latest solution for 2021...
I found that the project nanoid provides unique string ids that can be used as key while also being fast and very small.
After installing using npm install nanoid, use as follows:
import { nanoid } from 'nanoid';
// Have the id associated with the data.
const todos = [{id: nanoid(), text: 'first todo'}];
// Then later, it can be rendered using a stable id as the key.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
Another option is weak-key: https://www.npmjs.com/package/weak-key
import weakKey from "weak-key";
const obj1 = {a : 42};
const obj2 = {b : 123};
const obj3 = {a : 42};
console.log(weakKey(obj1)); // 'weak-key-1'
console.log(weakKey(obj2)); // 'weak-key-2'
console.log(weakKey(obj3)); // 'weak-key-3'
console.log(weakKey(obj1)); // 'weak-key-1'
For a simple array of text-strings; I'm trying one of the two ways:
1. encodeURI which is available on both; NodeJS and browser
const WithEncoder = () => {
const getKey = useCallback((str, idx) => encodeURI(`${str},${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
2. window.btoa which is available only in browser.
const WithB2A = () => {
const getKey = useCallback((str, idx) => window.btoa(`${str}-${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
Depends on the situation, choose a uniqueId creator is ok when you just want render silly items, but if you render items like drag&drop etc and you haven't any uniqueId for each item, I recommend remap that data in your redux, mapper, wherever and add for each item an uniqueId (and not in the render like <Item key={...}) because React couldn't perform any check between renders (and with that all the benefits).
With that remapped that you can use that new Id in your Component.
Here is what I have done, it works for reordering, adding, editing and deleting. Once set the key is not changed, so no unnecessary re-render. One PROBLEM which may be a show stopper for some: it requires adding a property to your object at first render say "_reactKey".
Example for functional component in psuedo TS (ie it won't run in snippets):
interface IRow{
myData: string,
_reactKey?:number
}
export default function List(props: {
rows: Array<IRow>
}) {
const {myRows} = props;
const [nextKey, setNextKey] = useState(100);
const [rows, setRows] = useState<Array<IRow>|undefined>();
useEffect(function () {
if (myRows) {
for (let row of myRows){
if (!row._reactKey){
row._reactKey = nextKey;
setNextKey(nextKey+1);
}
}
setRows(myRows);
} else if (!rows) {
setRows([]);
}
}, [myRows, columns]);
addRow(){
let newRow = { blah, blah, _reactKey : nextKey};
setNextKey(nextKey+1);
rows.push(newRow);
setRows({...rows});
}
function MyRow(props:{row:IRow}){
const {row} = props;
return <tr><td>{row._reactKey}</td><td>row.myData</td></tr>
}
return <table>
<tr><th>Index</th><th>React Key</th><th>My Data</th></tr>
rows.map((row, key)=>{
return <MyRow key={row._reactKey} row={row} />
}
</table>
}
I don't use react too much, but the last time I saw this issue I just created a new state array, and tracked the keys there.
const [keys, setKeys] = useState([0]);
const [items, setItems] = useState([value: "", key: 0,])
Then when I add a new item to list, I get the last key from the keys array, add 1, then use setKeys to update the keys array. Something like this:
const addItemWithKey = () => {
// create a new array from the state variable
let newKeyArr = [...keys];
// create a new array from the state variable that needs to be tracked with keys
let newItemArr = [...items];
// get the last key value and add 1
let key = newKeyArr[newKeyArr.length-1] + 1;
newKeyArr.push(key);
newItemArr.push({value: "", key: key,});
// set the state variable
setKeys(newKeyArr);
setItems(newItemArr);
};
I don't worry about removing values from the keys array because it's only being used for iterating in the component, and we're trying to solve for the case where we remove an item from the list and/or add a new item. By getting the last number from the keys array and adding one, we should always have unique keys.
import React, {useState} from 'react';
import {SafeAreaView,ScrollView,StyleSheet,Text,View,Dimensions} from 'react-native';
const {width}=Dimensions.get('window');
function sayfalar(){
let pages=[]
for (let i = 0; i < 100; i++) {
pages.push(<View key={i} style={styles.pages}><Text>{i}</Text></View>)
}
return pages
}
const App=()=>{
return(
<View style={styles.container}>
<ScrollView horizontal={true} pagingEnabled={true}>
{sayfalar()}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container:{
flexDirection:'row',
flex:1
},
pages:{
width:width
}
})
export default App;
You can use react-html-id to generate uniq id easely : https://www.npmjs.com/package/react-html-id
Use the mapped index (i)
things.map((x,i) => {
<div key=i></div>
});
Hope this helps.
The fastest solution in 2021 is to use uniqid: Go to https://www.npmjs.com/package/uniqid for more info but to sum up:
First in your terminal and your project file: npm install uniqid
Import uniqid in your project
Use it in any key that you need!
uniqid = require('uniqid');
return(
<div>
<div key={ uniqid() } id={list.name}>
<h2 key={ uniqid() }>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
I am using this:
<div key={+new Date() + Math.random()}>

Resources