I'm building out something that allows users to upload N-number of data files. In service of this, I've added a button that will create an additional file upload option. This is done with a simple for loop in the render function (there is a selection option that only appears if certain conditions are met, which is the 'mergeColumnSelection' variable, you can ignore that but I'm including it in case it somehow ends up being relevant):
let renderedEnrichedDataFields = [];
for(let i = 0; i < this.state.enrichedData.length; i++) {
let mergeColumnSelection = ""
if(this.state.enrichedData[i] !== null) {
mergeColumnSelection = <div className="form-item__select form-item">
<label className="form-label" htmlFor="add-target-owner">Merging Column</label>
<div className="form-item__container">
<select onChange={(e) => {this.setEnrichedMergeColumn(e, i)}} defaultValue={this.state.csvColumns[0]}>
{mainDataColumns}
</select>
</div>
</div>
}
renderedEnrichedDataFields.push(
<div className="form-group">
<button onClick={() => {this.removeEnrichmentData(i)}} type="button" className="modal-close">
<Icon name="close" />
</button>
<div className="form-item">
<label className="form-label" htmlFor="add-target-csv">Enrichment Dataset</label>
<input
className="csv-input"
type="file"
accept="text/csv"
onChange={(e) => {this.acceptNewEnrichedDataFile(e, i)}}
/>
</div>
{mergeColumnSelection}
</div>
)
}
Basically, every time the button is pressed a new element is pushed into the enrichedData array in state. This causes the application to render an additional file input. When a user uploads a file, the placeholder element in the array is replaced with the file. When the user eventually submits the form, an array of files will be submitted which is great.
However! I'm having a lot of trouble getting a clean implementation for the ability to REMOVE these input fields. The function
removeEnrichmentData(index) {
let enrichmentData = this.state.enrichedData
let enrichedMergeColumns = this.state.enrichedMergeColumns;
enrichmentData.splice(index, 1);
enrichedMergeColumns.splice(index, 1)
this.setState({enrichedData: enrichmentData, enrichedMergeColumns: enrichedMergeColumns});
}
As you can see this takes the index of the selected input, then splices it from the array that generates the for loop. The appropriate file is spliced from the array, and the number of file inputs is correct. However, there are problems with what file name is displayed. Pictures will help:
Here you can see a sample where someone is preparing to upload three
files, health, cluster, and starbucks
Now I select to remove the cluster item (item 2) from the list. It is
removed from the file list in state, leaving just health and
starbucks. However, the for loop simply runs through twice and drops
the last item - meaning that it appears that the health and cluster
are the remaining two files, even though in actuality they are health
and starbucks
I thought about moving the JSX block itself into state so I can specifically target the JSX input element I want removed - but have had limited success with this approach and read that it's not advisable to put JSX into the state. React doesn't really have built in ways to easily delete the specific input, and I can't set default values in file inputs so I can't easily tie the individual inputs to their counterpart in state.
It feels like it should be such a simple problem and I'm very stuck. Any help is appreciated!
#Miloš Rašić is correct - your initial problem is that you're probably using array indices for the keys for the inputs. So, if you have 10 inputs numbered 0...9, and you delete the input with index 5, you're still rendering items with keys 0..8, and React thinks the last one was removed.
Per your comment about using UUIDs, it sounds like you're generating unique IDs in the render() method itself. DO NOT DO THAT! Never generate random values for keys in render(). When you do that, you're telling React every time that "this item is different than the last time we rendered, please destroy the existing item here and replace it with a new one".
Instead, you should generate these unique IDs when you add a new entry into your state. For example:
class FileInputList extends Component {
state = { inputs : [] }
addNewFileInput = () => {
const inputID = uuid();
const newInputs = this.state.inputs.concat({id : inputID});
this.setState({inputs : newInputs});
}
render() {
const {inputs} = this.state;
const inputList = inputs.map(inputEntry) => {
return <input type="file" key={inputEntry.id} />
});
return inputList;
}
}
It's hard to be sure without a fully working example, but this awfully similar to a "working" example of how React messes up when you don't give arrays of elements a key prop. Don't you get warnings about this from React? Try giving the divs you are pushing to the array a key prop which won't change for an existing element when an element is deleted (so key={i} won't work).
For example, if you are rendering
<input type="file" key={1} />
<input type="file" key={2} />
<input type="file" key={3} />
<input type="file" key={4} />
when you delete the one with key={2} it should be
<input type="file" key={1} />
<input type="file" key={3} />
<input type="file" key={4} />
Some sort of incrementing id like in relational databases or a generated unique id would do the trick.
Have you tried this:
{this.removeEnrichmentData(index)}}
Here is a working example.
Related
working on a ReactJs project. It is worth noting I am a uni student is very new to react. Part of the code contains an image that can be clicked like a button. Once clicked a react Modal element is opened and the contents of an array are displayed inside
Screen of arrays printed contents
As you will see from the above image, each array item starts with a different number. The issue is all the array elements are printed on one continuous line without spacing inbetween each element.
here is the code for the Modal
<button className="space boxButton button3" onClick={this.openGreenModal}>
<img className="boxImg" src={greenBox} />
<div onClick={e => e.stopPropagation()}>
<Modal className="ModalGreen" isOpen={this.state.greenVisible}>
<div>
<img className="boxImgPopUp" src={greenBox} />
<h1> Green box testing</h1>
//PRINTING ARRAY ITEMS ON THIS LINE
<p>Items: {this.state.greenArray}</p>
<button onClick={this.closeGreenModal}>Close</button>
</div>
</Modal>
</div>
</button>;
is there a way in which I can display each item on a new line?
if anymore code is needed for the project pls do let me know
Searching for "react render list" put me in the React documentation for exactly this sort of thing: https://reactjs.org/docs/lists-and-keys.html
To give an actual answer though, React is just creating HTML for you so you would want to create HTML tags to render items on different lines just like if you were creating that HTML by hand.
Something like:
{this.state.greenArray.map((item) =>
<p key={item.something_unique}>{item}<p>
)}
I've created a Row and a Col components for an Card in Accordion using Bootstrap - simple, not React.Bootstrap or whatever. Row and Col just show their children, the Card just has more props and also just shows the children. Like that:
class Col extends React.Component {render () {return (
<div className='col' id={'col_'+this.props.colid} key={shortid.generate()}>
{this.props.children}
</div>)}}
class Row extends React.Component {render () { return (
<div className='row' id={'row_'+this.props.rowid} key={shortid.generate()}>
{this.props.children}
</div>)}}
Then I want to insert a textarea, which is provided with a function to make some changes to the text and I display these changes within a div below the textarea. Also I've got a button, that copies the text to buffer, based on clipboard.js
The first version of render function works just fine, but it doesn't give me the needed design, so I came up with the second version, based on Row and Col components, described above.
The contents mainly does not differ - the same textarea, checkbox, button and div. What differs is the layout. In the first version there is no layout at all, in the second I tried my best :)
So:
render () { return (<React.Fragment>
<textarea id='xng0' onChange={this.handleChange} ref={this.xng0ref} />
<CopyButton target="st0" message="Copy text" /> <br />
<input type='checkbox' onChange={this.toggleTranslit}
defaultChecked={this.state.tranParam} /><span>Translit?</span><br />
<div id='st0' border='1' ref={this.st0ref} >
{this.state.st0value}
</div></React.Fragment>)}
The second version:
render () {return ( <React.Fragment><Row><Col>
<textarea id='xng0' onChange={this.handleChange} ref={this.xng0ref} />
</Col><Col><Row><Col>
<CopyButton target="st0" message="Copy text" />
</Col></Row>
<Row><Col>
<input type='checkbox' onChange={this.toggleTranslit}
defaultChecked={this.state.tranParam}
/><span>Translit?</span>
</Col></Row>
</Col>
</Row>
<Row>
<div id='st0' border='1' ref={this.st0ref}>
{this.state.st0value}
</div></Row></React.Fragment>)}
My problem is: while the first render() version keeps focus in the textarea, when I type, the second version throws the focus off the textarea after the first letter was typed AND clears the textarea. That is I have no possibility to input some long text - I get only one letter.
What did I miss? Why this happens and how to make the focus stable?
As you were already prompted in the comments, you have unique keys for each render on the Col and Row components, because key={shortid.generate()}.
After writing a character in the textarea, you most likely change the state, then rerender happens and in the place of the current textarea, a new one appears, naturally without focus.
I recommend that you carefully read why the keys are created in the React. Link https://reactjs.org/docs/lists-and-keys.html#keys.
I must say right away that they were created to identify the list items, which is completely violated when you generate unique keys on each render.
please add 'value' property to your textarea
The component model:
private SomeArray = [{ key: "Initial" }];
User can add/remove items dynamically:
addField() {
this.SomeArray.push({ key: Math.random().toString() });
}
removeField(index: number) {
this.SomeArray.splice(index, 1);
}
Template markup:
<div class="col-xs-12">
<button (click)="addField()" type="button">Add</button>
</div>
<div *ngFor="let field of SomeArray; let i = index;">
<input [(ngModel)]="field.key" #modelField="ngModel" [name]=" 'SomeArray['+i+'].key' " type="text" class="form-control" required />
<div [hidden]="modelField.pristine || !(modelField.errors && modelField.errors.required)" class="alert alert-danger">
Required error
</div>
<button (click)="removeField(i)" class="btn btn-danger">Remove</button>
</div>
This works untill user removes any item from SomeArray. If I add some two items initially:
and remove the one with 1 index:
then after adding another item Angular treat it as item has both 0 and 1 index (the new item "occupies" both two inputs):
(item with key 0.1345... is not displayed)
It's worth to noting items of SomeArray are as expected, but data binding fails. What can be the reason of it?
Update: Thanks to the comments of #Stefan Svrkota and #AJT_82 it's known for me the issue can be resolved by adding [ngModelOptions]="{standalone: true}" to the needed input. But I couldn't stop thinking about the reason of the issue in my cause, without setting standalone option (there is unique value for each name attribute so it's excepted nothing wrong here).
Finally I have found that behavior occurs when input elements are into <form> tag only - Plunker sample here (enclosing of template with form tag is the reason that issue).
Any ideas of this behavior?
The reason why it happens is ngFor mixes name properties when you delete some item.
When you use ngModel inside form each ngModel control will be added to form controls collection.
Let's see what happens if we have added three items and clicked on Remove the second
1) Step1 - SomeArray[1].key exists in collection controls
2) Step2 - SomeArray[1].key has been removed from controls collection
3) Step3 - Html looks like
4) Step4 We are adding a new item
So formGroup returns existing item.
How we can solve it?
1) Don't wrap our controls in form tag
2) Add ngNoForm attribute to form
<form ngNoForm>
3) Use
[ngModelOptions]="{standalone: true}
With all three solutions above:
We can remove [name] property binding
We can't use the built in Form group validation
4) Use trackBy for ngFor
template.html
<div *ngFor="let field of SomeArray; let i = index; trackBy: trackByFn">
component.ts
trackByFn(i: number) {
return i;
}
Plunker Example
This way our built in form will work properly
I'm using Preact (for all intents and purposes, React) to render a list of items, saved in a state array. Each item has a remove button next to it. My problem is: when the button is clicked, the proper item is removed (I verified this several time), but the items are re-rendered with the last item missing, and the removed one still there. My code (simplified):
import { h, Component } from 'preact';
import Package from './package';
export default class Packages extends Component {
constructor(props) {
super(props);
let packages = [
'a',
'b',
'c',
'd',
'e'
];
this.setState({packages: packages});
}
render () {
let packages = this.state.packages.map((tracking, i) => {
return (
<div className="package" key={i}>
<button onClick={this.removePackage.bind(this, tracking)}>X</button>
<Package tracking={tracking} />
</div>
);
});
return(
<div>
<div className="title">Packages</div>
<div className="packages">{packages}</div>
</div>
);
}
removePackage(tracking) {
this.setState({packages: this.state.packages.filter(e => e !== tracking)});
}
}
What am I doing wrong? Do I need to actively re-render somehow? Is this an n+1 case somehow?
Clarification: My problem is not with the synchronicity of state. In the list above, if I elect to remove 'c', the state is updated correctly to ['a','b','d','e'], but the components rendered are ['a','b','c','d']. At every call to removePackage the correct one is removed from the array, the proper state is shown, but a wrong list is rendered. (I removed the console.log statements, so it won't seem like they are my problem).
This is a classic issue that is totally underserved by Preact's documentation, so I'd like to personally apologize for that! We're always looking for help writing better documentation if anyone is interested.
What has happened here is that you're using the index of your Array as a key (in your map within render). This actually just emulates how a VDOM diff works by default - the keys are always 0-n where n is the array length, so removing any item simply drops the last key off the list.
Explanation: Keys transcend renders
In your example, imagine how the (Virtual) DOM will look on the initial render, and then after removing item "b" (index 3). Below, let's pretend your list is only 3 items long (['a', 'b', 'c']):
Here's what the initial render produces:
<div>
<div className="title">Packages</div>
<div className="packages">
<div className="package" key={0}>
<button>X</button>
<Package tracking="a" />
</div>
<div className="package" key={1}>
<button>X</button>
<Package tracking="b" />
</div>
<div className="package" key={2}>
<button>X</button>
<Package tracking="c" />
</div>
</div>
</div>
Now when we click "X" on the second item in the list, "b" is passed to removePackage(), which sets state.packages to ['a', 'c']. That triggers our render, which produces the following (Virtual) DOM:
<div>
<div className="title">Packages</div>
<div className="packages">
<div className="package" key={0}>
<button>X</button>
<Package tracking="a" />
</div>
<div className="package" key={1}>
<button>X</button>
<Package tracking="c" />
</div>
</div>
</div>
Since the VDOM library only knows about the new structure you give it on each render (not how to change from the old structure to the new), what the keys have done is basically tell it that items 0 and 1 remained in-place - we know this is incorrect, because we wanted the item at index 1 to be removed.
Remember: key takes precedence over the default child diff reordering semantics. In this example, because key is always just the 0-based array index, the last item (key=2) just gets dropped off because it's the one missing from the subsequent render.
The Fix
So, to fix your example - you should use something that identifies the item rather than its offset as your key. This can be the item itself (any value is acceptable as a key), or an .id property (preferred because it avoids scattering object references around which can prevent GC):
let packages = this.state.packages.map((tracking, i) => {
return (
// ↙️ a better key fixes it :)
<div className="package" key={tracking}>
<button onClick={this.removePackage.bind(this, tracking)}>X</button>
<Package tracking={tracking} />
</div>
);
});
Whew, that was a lot more long-winded that I had intended it to be.
TL,DR: never use an array index (iteration index) as key. At best it's mimicking the default behavior (top-down child reordering), but more often it just pushes all diffing onto the last child.
edit: #tommy recommended this excellent link to the eslint-plugin-react docs, which does a better job explaining it than I did above.
I have three react components rendering checkbox groups.
Queries -> renders a group
Query -> renders checkboxes
QueryItem -> render single input[type=checkbox]
I pass down the data coming from Queries as props for the children:
<div className="group queries">
{component.state.groups.map((group, index) => {
return <Query key={group.key} data={group} />
})}
</div>
Then in Query:
<div>
<h1>{component.props.data.label}</h1>
<ul>
{component.props.data.queries.map( (query, index) => {
return <QueryItem key={query.key} data={query} />
})}
</ul>
</div>
And finally in the QueryItem component:
<li>
<input type='checkbox'
ref="input"
defaultChecked={component.props.data.checked}
onChange={component.checkChanged}
name={component.props.data.id}
id={component.props.data.id} />
<label htmlFor={component.props.data.id}>
{component.props.data.label} <em>({component.props.data.total})</em>
</label>
</li>
The data gets rendered correctly. However, when the onChange (checkChanged function) gets invoked:
checkChanged (e) {
console.log(component.props.data)
}
the data in component.props.data is the data from the last QueryItem that was rendered. So, basically, the data is always the same and does not represent the data that was rendered.
You can see, I use the key attribute to make sure the component is unique. If I inspect the data-reactid, I can see they are unique.
What is going on? What am I doing wrong in the type of setup?
Goal of this setup is to invoke a data-call when a checkbox is changed and then trickling down the data from Queries to Query to QueryItem.
Did you by accident declare the variable 'outside the class'? This can lead to unexpected behaviour. Try to initialize all variables properly and see if the problem persists