Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l
Related
Let's say I have a <SelectPicker/> component where I can select an option. What I want is how to add another <SelectPicker/> after I selected an option.
function DynamicComponent() {
const [state, setState] = useState([
{ name: null, id: '1' },
]);
const handleAdd = (value) => {
// Updating logic
};
return(
<>
{ state.map(item => {
return <SelectPicker
onSelect={handleAdd}
key={item.id}
value={item.name}
data={options} />
})
}
</>
);
}
In the example above, let's say there is default SelectPicker which is not selected. After selection, I think handleAdd function should update object that has id equal to '1' and add another object like this { name: null, id: '2' }.
What is the best way to achieve such functionality in react? Any help would be appreciated.
Thank you.
On an abstract level, what you want to do is have an array of components inside your state which is then called by the Render function of DynamicComponent. This array should get its first SelectPicker component immediately, and every time handleAdd is called you add a new SelectPicker to the array using your setState function. You can get the id for each new SelectPicker component by finding array.length.
In addition to the question, the below note from OP is also addressed in the below question
what if I want to update object's name property that has id:1 and add
new object to state at the same time?
This may be one possible solution to achieve the desired result:
function DynamicComponent() {
const [myState, setMyState] = useState([
{name: null, id: '1'}
]);
const handleAdd = arrIdx => setMyState(prev => {
const newArr = [...prev];
prev[arrIdx]?.name = ">>>>----new name goes here---<<<<";
return [
...newArr,
{
name: null,
id: (prev.length + 1).toString()
}
]
});
return(
<div>
{myState.map((item, idx) => (
<SelectPicker
onSelect={() => handleAdd(idx)}
key={item.id}
value={item.name}
data={options}
/>
)}
</div>
);
}
NOTES
Avoid using variable-names such as "state"
Passed the "index" from the .map() iteration
This helps in tracking the exact array-element
The new element with name: null is added as the last
The id is calculated by incrementing the current length by 1
I have a react component that stores a set of fruits in useState. I have a memoized function (visibleFruits) that filters fruits. I map visibleFruits to the dom.
The problem is, when i check a fruit, all visible fruits re-render.
I am expecting that only the selected one re-renders since it is the only one that is changing.
Is there a way for me to use this pattern but prevent all from re-rendering on check?
In real life, there is a complex function in visibleFruits useMemo. So I can't simply append the filter before the map.
Edit, here is updated edit:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
])
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible)
}, [fruits])
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
f.selected = bool
}
return f
})
})
}
return (
<div>
{visibleFruits.map((fruit) => {
return <FruitOption fruit={fruit} handleCheck={handleCheck} />
})}
</div>
)
}
const FruitOption = ({ fruit, handleCheck }) => {
console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
)
}
export default Example
First, there's a problem with the handleCheck function (but it's not related to what you're asking about). Your code is modifying a fruit object directly (f.selected = bool), but you're not allowed to do that with React state, objects in state must not be directly modified, and rendering may not be correct if you break that rule. Instead, you need to copy the object and modify the copy (like you are with the array):
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
};
But that's not what you're asking about, just something else to fix. :-)
The reason you see the console.log executed twice after handleCheck is that the component has to be re-rendered (for the change), and there are two visible fruits, so you see two calls to your FruitOption component function. There are two reasons for this:
handleChange changes every time your Example component function is called, so FruitOption sees a new prop every time; and
FruitOption doesn't avoid re-rendering when its props don't change, so even once you've fixed #1, you'd still see two console.log calls; and
Separately, there's no key on the FruitOption elements, which can cause rendering issues. Always include a meaningful key when rendering elements in an array. (Don't just use the index, it's problematic; but your fruit objects have an id, which is perfect.)
To fix it:
Memoize handleChange so that it's not recreated every time, probably via useCallback, and
Use React.memo so that FruitOption doesn't get called if its props don't change (see the end of this answer for the class component equivalent), and
Add a meaningful key to the FruitOption elements in Example
Taking those and the handleChange fix above and putting them all together:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
]);
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible);
}, [fruits]);
const handleCheck = React.useCallback(
(bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
},
[] // *** No dependencies since all it uses is `setFruits`, which is
// stable for the lifetime of the component
);
return (
<div>
{visibleFruits.map((fruit) => {
// *** Note the key
return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
})}
</div>
);
}
// *** `React.memo` will compare the props and skip the call if they're the same, reusing
// the previous call's result.
const FruitOption = React.memo(({ fruit, handleCheck }) => {
console.log(`Rendering fruit ${fruit.id}`);
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
);
});
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
As you can see, with all that in place, only the changed fruit is re-rendered.
Re React.memo: For components with more complicated requirements, you can provide a function as a second argument that determines whether the two sets of props are "the same" for rendering purposes. By default, React.memo just does a shallow equality comparison, which is often sufficient.
Finally: For class components, the equivalent of React.memo without providing an equality callback is extending PureComponent instead of Component. If you want to make your check of the props more fine-grained, you can implement shouldComponentUpdate instead.
I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?
New to React.
I have a handler, as follows, that updates state of an array. The data is a set of animal pairs.
class Animal extends Component {
state = {
pairs: [
{ fromAnimal: 'Dog', toAnimal: 'Cat' },
{ fromAnimal: 'Lion', toAnimal: 'Tiger' },
{ fromAnimal: 'Rabbit', toAnimal: 'Bear' }
]
};
closePairHandler = (fromAnimal, toAnimal) => {
let newPairs = this.state.pairs.filter((pair) => {
return !(pair.fromAnimal === fromAnimal && pair.toAnimal === toAnimal);
});
console.log('pairs', newPairs); // This shows that the correct pair was removed from the array.
this.setState({ pairs: newPairs });
};
render() {
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);
};
};
export default Animal;
This is a super simplified version of the code I have. BUT, when the closePairHandler is called to remove an animal pair (for example, Lion/Tiger). The console.log in the closePairHandler shows that the array has been updated successfully.
However, when the components render. It is removing the LAST component in the array and not the one that was selected. It's reducing the array size by 1, but not removing the correct item in the mapping (in render), althought the closePairHandler console.log is showing the array correctly updated before setting the state.
Can anyone explain to me what is going on here?
Thanks again!
You are not providing the key for your mapped data while rendering SomeComponent and hence react is not able to correctly identify what element got changed. You can use index as the key if you don't have a unique id in your pair object else you should use that for performance reasons
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);
i have an issue trying to create a "typehead" funcitonality in my app, i have an "input" that listen to onChange, and that onChange is calling to a Redux reducer that search for a tag in hole store, i need to retrive all matches with my search, over here everything is ok, but when i delete my search, my hole store is equals to my filtered results, and i want that when my search is empty it returns hole my store. (gif and code)
case 'SEARCH_BY_TAG':
let tag = action.tag
let filtered = state.slice(0)
if(tag != ""){
const copied = state.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(tag) > -1){
return true
}
})
})
return filtered = copied.filter(Boolean)
}else{
return filtered
}
break;
Instead of filtering things out inside your reducer, do it directly on render(), there is nothing wrong with that.
You can still use the SEARCH_BY_TAG action to keep track of the search keyword and use it to apply the filter when rendering your list.
I think you should refactor your state to change it from this:
[ item1, item2, item3, ... ]
to this:
{
query: '',
options: [ item1, item2, item3, ... ]
}
This way you can do what #Raspo said in his answer - do the filtering of the options in your render function.
Currently when you change the text in the search field, you are dispatching this action:
{
type: 'SEARCH_BY_TAG',
tag: 'new query'
}
I think you should change the action name, and the reducer code, to look more like this:
// note this is almost exactly the same as the old action
{
type: 'CHANGE_AUTOCOMPLETE_QUERY',
query: 'new query'
}
and the reducer could then change to this:
case CHANGE_AUTOCOMPLETE_QUERY:
return Object.assign({}, state, {
query: action.query
})
Note that in the reducer case I just wrote, the options part of the state isn't changed at all. It remains constant.
Now let's assume your current render function looks something like this:
const options = reduxState // not sure exactly how you get the state but whatever
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)
This code relies on getting the state as an array. You could change it in the new setup to do this:
const query = reduxState.query
const options = reduxState.options.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(query) > -1){
return true
}
})
})
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)