I have a simple table of ingredients where by default three rows are generated and maintained by React useState [ingredients,setIngredients]. Typing / changing the text with trigger onChange function that in turn run setIngredients.
When I type in the textbox, it loses focus and caret disappears. I have added a unique key to each input and yet it still loses focus. If I inspect and look at the elements, it seems that the entire <tbody> rerenders.
import React, { Fragment, useState } from 'react';
const IngredientsTable = () => {
const [rowsGenerated, setRowsGenerated] = useState(3);
const [ingredients, setIngredients] = useState({
1: { id: '1', name: '', proportion: '' },
2: { id: '2', name: '', proportion: '' },
3: { id: '3', name: '', proportion: '' },
});
const onFieldChange = ({ value, id, field }) => {
console.log('onFieldChange triggered', { id, value, field });
setIngredients((prevState) => ({
...prevState,
[id]: { ...prevState[id], [field]: value },
}));
};
const Row = ({ rowId, name, proportion }) => (
<Fragment>
<tr>
<td>
<input
id={rowId + 'name'}
key={rowId + 'name'}
type="text"
value={name}
onChange={(e) =>
onFieldChange({
id: rowId,
field: 'name',
value: e.target.value,
})
}
/>
</td>
<td>
<input
id={rowId + 'proportion'}
key={rowId + 'proportion'}
type="text"
value={proportion}
onChange={(e) =>
onFieldChange({
id: rowId,
field: 'proportion',
value: e.target.value,
})
}
/>
</td>
<td>
<button type="button">-</button>
</td>
</tr>
</Fragment>
);
const addRow = () => {
setRowsGenerated((prevCount) => prevCount + 1);
setIngredients((prevState) => ({
...prevState,
[rowsGenerated + 1]: { id: rowsGenerated + 1, name: '', proportion: '' },
}));
};
return (
<div className="ingredient_table">
<table>
<thead>
<tr>
<th style={{ width: '62%' }}>Name </th>
<th style={{ width: '30%' }}>Proportion %</th>
<th style={{ width: '8%' }}></th>
</tr>
</thead>
<tbody>
{Object.keys(ingredients).map((id, i) => {
const rowId = id;
return (
<Row
key={`ingredient${rowId}`}
rowId={rowId}
name={ingredients[rowId].name}
proportion={ingredients[rowId].proportion}
/>
);
})}
</tbody>
</table>
<button className="ingredient_add_row_button" onClick={addRow}>
Add row
</button>
</div>
);
};
export default IngredientsTable;
>Solution :
You are creating a <Row> component inside the <IngredientsTable> component, this causes issues, move the <Row> outside the other component and your issue is fixed
const { useState, useEffect } = React;
const Row = ({ rowId, name, proportion, onFieldChange }) => (
<React.Fragment>
<tr>
<td>
<input
id={rowId + 'name'}
key={rowId + 'name'}
type="text"
value={name}
onChange={(e) =>
onFieldChange({
id: rowId,
field: 'name',
value: e.target.value,
})
}
/>
</td>
<td>
<input
id={rowId + 'proportion'}
key={rowId + 'proportion'}
type="text"
value={proportion}
onChange={(e) =>
onFieldChange({
id: rowId,
field: 'proportion',
value: e.target.value,
})
}
/>
</td>
<td>
<button type="button">-</button>
</td>
</tr>
</React.Fragment>
);
const IngredientsTable = () => {
const [rowsGenerated, setRowsGenerated] = useState(3);
const [ingredients, setIngredients] = useState({
1: { id: '1', name: '', proportion: '' },
2: { id: '2', name: '', proportion: '' },
3: { id: '3', name: '', proportion: '' },
});
const onFieldChange = ({ value, id, field }) => {
//console.log('onFieldChange triggered', { id, value, field });
setIngredients((prevState) => ({
...prevState,
[id]: { ...prevState[id], [field]: value },
}));
};
const addRow = () => {
setRowsGenerated((prevCount) => prevCount + 1);
setIngredients((prevState) => ({
...prevState,
[rowsGenerated + 1]: { id: rowsGenerated + 1, name: '', proportion: '' },
}));
};
return (
<div className="ingredient_table">
<table>
<thead>
<tr>
<th style={{ width: '62%' }}>Name </th>
<th style={{ width: '30%' }}>Proportion %</th>
<th style={{ width: '8%' }}></th>
</tr>
</thead>
<tbody>
{Object.keys(ingredients).map((id, i) => {
const rowId = id;
return (
<Row
key={`ingredient${rowId}`}
rowId={rowId}
name={ingredients[rowId].name}
proportion={ingredients[rowId].proportion}
onFieldChange={onFieldChange}
/>
);
})}
</tbody>
</table>
<button className="ingredient_add_row_button" onClick={addRow}>
Add row
</button>
</div>
);
};
ReactDOM.render(<IngredientsTable />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>