React Native infinite loop with object array in useEffect - javascript

In my project, I need to get selected items from a Flatlist and pass them to my parent component.
I created a local state like this:
const [myState, setMyState] = useState<IStateType[] | []>([])
Each time an item is selected I try to add it to my useEffect:
useEffect(() => {
const result = myState.filter((el) => el.id !== item.id)
if (isSelected) {
setMyState([
...result,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
But I would need to put mySate in the dependency of my useEffect to add each time the new items selected. If I add it to the useEffect dependency it causes an infinite loop ^^
How to add each new item to my array while listening to all the changes and without causing an infinite loop?

I believe the issue you're having it's because you're not separating the concerns of each component correctly, once you have to relay on the previous data every time, the useEffect can be tricky. But there are two solutions to your issue:
Make use of useState callback function:
The useState function can be used with a callback rather than a value, as follows:
useEffect(() => {
if (isSelected) {
setMyState(prevState => [
...prevState,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
Best structure of your components + using useState callback function
What I could see about your approach is that you (as you showed) seems to be trying to handle the isSelected for each item and the myState in the same component, which could be done, but it's non-ideal. So I propose the creation of two components, let's say:
<List />: Should handle the callback for selecting an item and rendering them.
<List />:
function List() {
const [myState, setMyState] = useState([]);
const isItemSelected = useCallback(
(itemId) => myState.some((el) => el.id === itemId),
[myState]
);
const handleSelectItem = useCallback(
(itemId) => {
const isSelected = isItemSelected(itemId);
if (isSelected) {
setMyState((prevState) => prevState.filter((el) => el.id !== itemId));
} else {
setMyState((prevState) => prevState.concat({ id: itemId }));
}
},
[isItemSelected]
);
return (
<div>
<p>{renderTimes ?? 0}</p>
{items.map((item) => (
<Item
item={item}
onSelectItem={handleSelectItem}
selected={isItemSelected(item.id)}
/>
))}
</div>
);
}
<Item />: Should handle the isSelected field internally for each item.
<Item />:
const Item = ({ item, selected = false, onSelectItem }) => {
const [isSelected, setIsSelected] = useState(false);
useEffect(() => {
setIsSelected(selected);
}, [selected]);
return (
<div>
<p>
{item.name} is {isSelected ? "selected" : "not selected"}
</p>
<button onClick={() => onClick(item.id)}>
{isSelected ? "Remove" : "Select"} this item
</button>
</div>
);
};
Here's a codesnack where I added a function that counts the renders, so you can check the performance of your solution.

Related

Deleting a targeted row in function component React

I want to render a list of rows that can be added to and deleted dynamically. I have 2 components that handle adding and removing rows: ClassComponent and FunctionComponent. ClassComponent works as intended, but FunctionComponent only deletes the first row. The rows are stored as state and are updated using setState(). How can I make FunctionComponent logically equivalent to ClassComponent?
I'm using the library uuid to create unique keys.
import React from "react";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
export default function App() {
return (
<div>
<ClassComponent />
<FunctionComponent />
</div>
);
}
class ClassComponent extends React.Component {
state = {
list: [{ id: uuidv4() }, { id: uuidv4() }],
};
handleDelete = (id) => {
this.setState((prevState) => ({
list: prevState.list.filter((row) => row.id !== id),
}));
};
handleAdd = () => {
this.setState((prevState) => ({
list: [...prevState.list, { id: uuidv4() }],
}));
};
render() {
const { list } = this.state;
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={this.handleDelete}>
{id}
</Row>
))}
</ul>
<button onClick={this.handleAdd}>Add</button>
</>
);
}
}
const FunctionComponent = () => {
const [list, setList] = useState([{ id: uuidv4() }, { id: uuidv4() }]);
const handleDelete = (id) => {
console.log(id);
const copy = list.slice();
copy.splice(id, 1);
setList(copy);
};
const handleAdd = () => {
setList([...list, { id: uuidv4() }]);
};
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={handleDelete}>
{id}
</Row>
))}
</ul>
<button onClick={handleAdd}>Add</button>
</>
);
};
const Row = ({ onClick, children, id }) => (
<li>
{children} <button onClick={() => onClick(id)}>Delete</button>
</li>
);
Is there any reason not to use array.prototype.filter() just like you've done in your Class Component? This seems more readable and ensures you avoid mutating state directly in fewer steps.
Here is what that would look like in your handleDelete function in FunctionComponent:
const handleDelete = (id) => {
setList(list.filter((row) => (
row.id !== id
)));
};
In your FunctionalComponent you need to give index of the entry to splice method. Try like below.
const handleDelete = (id) => {
const copy = list.slice();
// find the index
const index = copy.findIndex(({ id: ID }) => id === ID);
// do the deletiong using that index
copy.splice(index, 1);
setList(copy);
};
You have to review your usage of the splice Array prototype.
copy.splice(id, 1);
Splice need to be passed as first argument an index and at second argument a delete count as such:
splice(start, deleteCount)
and you are passing an id to it. Actually in your case you are passing undefined as you're calling the function without argument.
I guess you could use the index of the map function to make it work:
{list.map(({ id }, index) => (
<Row key={id} id={id} onClick={() => handleDelete(index)}>
{id}
</Row>
))}
In your case you were passing undefined as id and 1 as the deleteCount so it was always deleting the first item.
in the functional component, you did not pass the id!
const FunctionalComponent = () => {
const [list, setList] = useState([{ id: uuidv4() }, { id: uuidv4() }]);
const handleDelete = (id) => {
// get all object except the id
const newRows = list.filter((i) => i.id !== id);
setList(newRows);
};
const handleAdd = () => {
setList([...list, { id: uuidv4() }]);
};
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={() => handleDelete(id)}>
{id}
</Row>
))}
</ul>
<button onClick={handleAdd}>Add</button>
</>
);
};

Reactjs prevent unwanted renderings with useCallback and memo

I have an array of objects called data. I loop this array and render the Counter component. Increment and decrement of the counter value are passed as props to the component.
But if I change the value in a one-component, the other two components also re-renders. Which is not needed. How do I prevent this behavior? I tried memo and useCallback but seems not implemented correctly.
Counter.js
import React, { useEffect } from "react";
const Counter = ({ value, onDecrement, onIncrement, id }) => {
useEffect(() => {
console.log("Function updated!", id);
}, [onDecrement, onIncrement]);
return (
<div>
{value}
<button onClick={() => onDecrement(id)}>-</button>
<button onClick={() => onIncrement(id)}>+</button>
</div>
);
};
export default React.memo(Counter);
Home.js
import React, { useState, useCallback } from "react";
import Counter from "../components/Counter";
export default function Home() {
const [data, setData] = useState([
{
id: 1,
value: 0,
},
{
id: 2,
value: 0,
},
{
id: 3,
value: 0,
},
]);
const onIncrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value += 1;
}
return record;
})
);
},
[data]
);
const onDecrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value -= 1;
}
return record;
})
);
},
[data]
);
return (
<div>
<h1>Home</h1>
{data.map((e) => {
return (
<Counter
value={e.value}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
);
})}
</div>
);
}
I suspect useCallback & useMemo are not helpful in this case, since you're running an inline function in your render:
{data.map(e => <Counter ...>)}
This function will always returns a fresh array & the component will always be different than the previous one.
In order to fix this, I think you'd want to memoize that render function, not the Counter component.
Here's a simple memoized render function with useRef:
// inside of a React component
const cacheRef = useRef({})
const renderCounters = (data) => {
let results = []
data.forEach(e => {
const key = `${e.id}-${e.value}`
const component = cacheRef.current[key] || <Counter
value={e.value}
key={e.id}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
results.push(component)
cacheRef.current[key] = component
})
return results
}
return (
<div>
<h1>Home</h1>
{renderCounters(data)}
</div>
);
In the codesandbox below, only the clicked component log its id:
https://codesandbox.io/s/vibrant-wildflower-0djo4?file=/src/App.js
Disclaimer: With this implementation, the component will only rerender if its data value changes. Other props (such as the increment/decrement callbacks) will not trigger changes. There's also no way to clear the cache.
Memoize is also trading memory for performance — sometimes it's not worth it. If there could be thousands of Counter, there're better optimiztion i.e. changing UI design, virtualizing the list, etc.
I'm sure there's a way to do this with useMemo/React.memo but I'm not familiar with it

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

useState variable not latest version in delete function, React

I have a list of div's that contain question fields. For every button click i add a new line of question fields and memorize in state the whole list and how many lines there are. I tried adding a delete button but when my delete functions it seems that the value from the state variable is remembered from when the line was made. How do i solve this so i can acces the full list in the HandleDelete function?
const OefeningAanvragenDagboek = () => {
const { t, i18n } = useTranslation()
const [questionCount, setQuestionCount] = useState(1)
const [questionList, setQuestionList] = useState([])
const createNewLine = () =>{
var newLine=
<div>
<Field type="text" name={"vraag" + questionCount}/>
<Field component="select" name={"antwoordMogelijkheid"+questionCount}>
<option value="">...</option>
<option value="open">{t('oefeningAanvragenDagboek.open')}</option>
<option value="schaal">{t('oefeningAanvragenDagboek.scale')}</option>
</Field>
<Field type="text" name={"type"+questionCount}/>
<button onClick={() => HandleDelete(questionCount-1)}>{t('assignmentCard.delete')}</button>
</div>
setQuestionList(questionList => [...questionList, newLine])
setQuestionCount(questionCount+1)
}
const HandleDelete = (index)=> {
console.log(questionList)
// setQuestionList(questionList.splice(index, 1))
}
return (
<div>
<button onClick={createNewLine}>{t('oefeningAanvragenDagboek.addQuestion')}</button>
{questionList}
</div>
)
}
Use functional setState as HandleDelete has closure on questionList
setQuestionList(questionList => questionList.splice(index, 1))
Both state and props received by the updater function are guaranteed to be up-to-date.
setState() in classes
You can pass event handler from container to child and then invoke event handler from client.
For example, let's say I have an app displaying list of items and each item have a delete button to remove them from the list. In this case, parent component will supply list of items and event handler to child component and then, child will be responsible for rendering and calling event handler.
Take a look at this codesandbox from which I am pasting following code:
import React, { useState } from "react";
import "./styles.css";
export function List(props) {
return (
<div>
{props.items.map((i, idx) => (
<div class="item" key={idx}>
{i} <span onClick={() => props.onDelete(idx)}>X</span>
</div>
))}
</div>
);
}
export default function App() {
const [items, setItems] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6"
]);
const deleteItem = (index) => {
if (index >= 0 && index < items.length) {
const newItems = items.slice();
newItems.splice(index, 1);
setItems(newItems);
}
};
return (
<div className="App">
<List items={items} onDelete={deleteItem}></List>
</div>
);
}
Primarily addressing the issue on the OP's comments section, at the time of this writing which was given a bounty in addition to the question.
The SandBox with the issue: https://codesandbox.io/s/charming-architecture-9kp71?file=/src/App.js
Basically, the solution to this issue is to perform all operations on the parameter of the callback function. In the case of the sandbox issue I linked above, if you look at removeToast on the code below, the operations are being done on the list array.
Code with the issue:
export default function App() {
const [list, setList] = useState([]);
const removeToast = (id) => {
const newList = list.filter(({ toastId }) => toastId !== id);
setList([...newList]);
};
const addToast = () => {
const toastId = Math.random().toString(36).substr(2, 9);
const newList = [
...list,
{
toastId,
content: (
<>
<button onClick={() => removeToast(toastId)}>Hi there</button>
Hello {toastId}
</>
)
}
];
setList([...newList]);
};
return (
<>
<button onClick={addToast}>Show Toast</button>
<Toaster removeToast={removeToast} toastList={list} />
</>
);
}
However since removeToast has a closure on list, we need to do the filtering on the previous state which is, again, accessible via the first parameter of the callback of setState
The fix:
const removeToast = (id) => {
setList((prev) => {
return prev.filter(({ toastId }) => toastId !== id);
});
};
The Solution: https://codesandbox.io/s/suspicious-silence-r1n41?file=/src/App.js

React Hooks - How to use a make a state dependent on another state?

I recently started using hooks in react and I am often having this problem: I create a first big state which is used by all my component, but some of the smaller parts of my component divide this state and create their own state for simplicity.
For example
import React, { useState } from "react";
const initialFilters = {
name: "",
code: ""
};
function Filter({ value, setFilters }) {
const [tempValue, setTempValue] = useState(value);
return (
<input
value={tempValue}
onChange={e => setTempValue(e.target.value)}
onBlur={() => setFilters(tempValue)}
/>
);
}
function App() {
const [filters, setFilters] = useState(initialFilters);
const agents = [
{ name: "bob", code: "123" },
{ name: "burger", code: "3123" },
{ name: "sponge", code: "34" }
];
return (
<div>
<label>Name filter</label>
<Filter
value={filters.name}
setFilters={value =>
setFilters(filters => ({ ...filters, name: value }))
}
/>
<label>Code filter</label>
<Filter
value={filters.code}
setFilters={value =>
setFilters(filters => ({ ...filters, code: value }))
}
/>
<button onClick={() => setFilters(initialFilters)}>Reset filters</button>
<ul>
{agents
.filter(
agent =>
agent.name.includes(filters.name) &&
agent.code.includes(filters.code)
)
.map((agent, i) => (
<li key={i}>
name: {agent.name} - code: {agent.code}
</li>
))}
</ul>
</div>
);
}
export default App;
CodeSandox available here
In this example the filters work fine, but their value are not emptied when we use the button Reset.
The filters create their own states to dispatch new state only on blur, and still be controlled. I guess I could use ref here, but I use this example to showcase a simple case of state dependent on another state (and therefore on props).
How should I go about implementing this in an idiomatic React way?
You can use a useEffect hook. The 1st argument is a function and the 2nd argument is an array of dependencies. When a dependency changes value, the function is executed again.
import { useEffect } from 'react';
// ....code removed....
useEffect(() => {
setTempValue(value);
}, [value]);
// ....code removed....
Sandbox with changes: https://codesandbox.io/s/kind-bogdan-ljugv
As you can read in the documentation (https://reactjs.org/docs/hooks-state.html#declaring-a-state-variable), your state is created on the first render only and only then it is equal to your initial value.
You could write a custom hook useFilter and expose your filter-reseter:
const useFilter = (value, setFilters) => {
const [tempValue, setTempValue] = useState(value);
const resetFilter = () => setTempValue(value)
return {
resetFilter,
getInputProps: () => ({
onChange: e => setTempValue(e.target.value),
onBlur: () => setFilters(tempValue),
value: tempValue,
})
}
and instead of doing:
<Filter
value={filters.name}
setFilters={value =>
setFilters(filters => ({ ...filters, name: value }))
}
/>
do this:
const setFilters = value => setFilters(filters => ({ ...filters, name: value }))
const { resetTempFilter, getInputProps } = useFilter(value, setFilters)
...
<input {...getInputProps()} />
In this case you much easier to re-instantiate children with changing key prop(so named "reset state" technique based on how reconciliation works in React):
const [resetKey, setResetKey] = useState(0);
const doReset = setResetKey(key => key + 1);
<Filter
key={`name-filter-${resetKey}`}
...
/>
<Filter
key={`code-filter-${resetKey}`}
...
/>
<button onClick={doReset}>Reset!</button>
It's not only easier to achieve. It also will work the same for any stateful components you cannot modify(for any reason).

Categories