Customize Array State in ReactJs with ES6 - reactjs

i am fetching some data (in array) from api and assigning it to state called items. then calling an Item component for each item of the state by following code:
<div>
{items.length} results found for {search_term}
{items.map((item) =>
(item.active_price > 0 && item.show) ? <Item key={item.id} details={item} items={items} setItems={setItems}/> : ""
)}
</div>
The array looks like this:
Next plan is adding a remove button in Item component, which will change value of show to false (for the item clicked) in the items state. What I am thinking is, as the state will change, the items state will be re-rendered and the item turned to false will be hidden from view. I am adding a onCLick event listener on the button and the function is:
const handleClick = () => {
console.log(({...props.items,[props.details.number - 1]:false}))
// props.setItems(prevState=>({...prevState,[props.details.number - 1]:false}))
}
I'll call props.setItems later but for now, I am trying to see if I can just edit that element to turn the show into false. the code above replace the whole index with false.
in the onClick function, I only want to edit the value of show, not replace the entire entry. I have tried:
({...props.items,[props.details.number - 1][show]:false})
and
({...props.items,[props.details.number - 1].show:false})
It shows syntax error before [show] and .show respectively. How can I edit my handleClick function to edit the value of show properly.
Thanks.

It looks like you are converting your array into an object in your current method. (the first screen shot is an array, the second is an object). You're going to run into issues there.
To just remove the item from the array, it would be easiest just to use .filter();
const handleClick = () => {
props.setItems(
prevState => prevState.filter(item => item.number !== props.details.number)
);
}
To set the show property is a bit more complicated. This is pretty standard way to do so without mutating.
const handleClick = () => {
props.setItems(prevState => {
const itemIndex = prevState.findIndex(
item => item.number == props.details.number
);
// Using 'props.details.number - 1' will not be a valid index when removing items
return [
...prevState.slice(0, itemIndex), // Your state before the item being edited
{...prevState[itemIndex], show: false}, // The edited item
...prevState.slice(itemIndex + 1) // The rest of the state after the item
];
});
}

You can try the following syntax of code, assuming items is state here-
const [items,setItems]=.....
The following is the code
const handleClick = () => {
setItems({
...items,
show: false,
})}
It will update the items state with value show as false.

Related

Make a momentary highlight inside a virtualized list when render is not triggered

I have an extensive list of items in an application, so it is rendered using a virtual list provided by react-virtuoso. The content of the list itself changes based on API calls made by a separate component. What I am trying to achieve is whenever a new item is added to the list, the list automatically scrolls to that item and then highlights it for a second.
What I managed to come up with is to have the other component place the id of the newly created item inside a context that the virtual list has access to. So the virtual list looks something like this:
function MyList(props) {
const { collection } = props;
const { getLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
// calling this function also resets the lastId inside the context,
// so next time it is called it will return undefined
// unless another item was entered
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
I'm using useRef instead of useState here because using a state breaks the whole thing - I guess because Virtuouso doesn't actually re-renders when it scrolls. With useRef everything actually works well. Inside ItemRow the highlight is managed like this:
function ItemRow(props) {
const { content, toHighlight, checkHighligh } = props;
const highlightMe = toHighlight;
useEffect(() => {
toHighlight && checkHighlight && checkHighligh();
});
return (
<div className={highlightMe ? 'highligh' : undefined}>
// ... The rest of the render
</div>
);
}
In CSS I defined for the highligh class a 1sec animation with a change in background-color.
Everything so far works exactly as I want it to, except for one issue that I couldn't figure out how to solve: if the list scrolls to a row that was out of frame, the highlight works well because that row gets rendered. However, if the row is already in-frame, react-virtuoso does not need to render it, and so, because I'm using a ref instead of a state, the highlight never gets called into action. As I mentioned above, using useState broke the entire thing so I ended up using useRef, but I don't know how to force a re-render of the needed row when already in view.
I kinda solved this issue. My solution is not the best, and in some rare cases doesn't highlight the row as I want, but it's the best I could come up with unless someone here has a better idea.
The core of the solution is in changing the idea behind the getLastId that is exposed by the context. Before it used to reset the id back to undefined as soon as it is drawn by the component in useEffect. Now, instead, the context exposes two functions - one function to get the id and another to reset it. Basically, it throws the responsibility of resetting it to the component. Behind the scenes, getLastId and resetLastId manipulate a ref object, not a state in order to prevent unnecessary renders. So, now, MyList component looks like this:
function MyList(props) {
const { collection } = props;
const { getLastId, resetLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
resetLastId();
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current === index || getLastId() === item.id}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
Now, setting the highlightIndex inside useEffect takes care of items outside the viewport, and feeding the getLastId call into the properties of each ItemRow takes care of those already in view.

Do I have a stale closure in my React app using react-dnd for drag and drop?

I am building a drag-n-drop application that lets users build up a tree of items that they've dragged onto a list.
When dragging items onto a list, there are two possible actions:
The item is dropped onto the list and added
The item is dropped onto an existing item and is added as a child of that item
There is a bug when adding child items. I have reproduced the bug in CodeSandbox here:
https://codesandbox.io/s/react-dnd-possible-stale-closure-2mpjju?file=/src/index.tsx**
Steps to reproduce the bug:
Open the CodeSandbox link
Drag 3 or more items onto the list
Drag an item and drop it onto the first or second item
You will see it successfully adds the item as a child, but the root items below it get removed
I will explain what I think is happening below, but here's an overview of the code:
A list of draggable items:
export function DraggableItems() {
const componentTypes = ["Car", "Truck", "Boat"];
return (
<div>
<h4>These can be dragged:</h4>
{componentTypes.map((x) => (
<DraggableItem itemType={x} />
))}
</div>
);
}
function DraggableItem({ itemType }: DraggableItemProps) {
const [, drag] = useDrag(() => ({
type: "Item",
item: { itemType: itemType }
}));
return <div ref={drag}>{itemType}</div>;
}
...these items can be dropped on two places: the DropPane and the previously DroppedItems.
DropPane:
export function DropPane() {
const [items, setItems] = useState<Array<Item>>([]);
const [, drop] = useDrop(
() => ({
accept: "Item",
drop: (droppedObj: any, monitor: any) => {
if (monitor.didDrop()) {
console.log("Drop already processed by nested dropzone");
} else {
const newItem = makeRandomItem(droppedObj.itemType);
setItems([...items, newItem]);
}
}
}),
[items]
);
const deleteItem = (idToDelete: number) => {
// a recursive function which filters out the item with idToDelete
const deleteItemRecursively = (list: Array<Item>) => {
const filteredList = list.filter((x) => x.id !== idToDelete);
if (filteredList.length < list.length) {
return filteredList;
} else {
return list.map((x) => {
x.children = deleteItemRecursively(x.children);
return x;
});
}
};
// the recursive function is called initially with the items state object
const listWithTargetDeleted = deleteItemRecursively(items);
setItems(listWithTargetDeleted);
};
const addItemAsChild = (child: Item, targetParent: Item) => {
// same as the delete function, this recursive function finds the correct
// parent and adds the child item to its list of children
const addItemAsChildRecursively = (list: Array<Item>) => {
return list.map((x) => {
if (x.id === targetParent.id) {
x.children.push(child);
return x;
} else {
x.children = addItemAsChildRecursively(x.children);
return x;
}
});
};
// it's called initially with the items state object
const reportComponentsWithChildAdded = addItemAsChildRecursively(items);
setItems(reportComponentsWithChildAdded);
};
return (
<div ref={drop} style={{ border: "1px solid black", marginTop: "2em" }}>
<h4>You can any items anywhere here to add them to the list:</h4>
{items.length === 0 || (
<DroppedItemList
items={items}
onDeleteClicked={deleteItem}
addItemAsChild={addItemAsChild}
indentation={0}
/>
)}
</div>
);
}
It's the addItemAsChild function that I believe may be causing the error, but I am not sure since if there is a stale closure - i.e. the items list is getting wrapped and passed into the DroppedItemList to be called - I would think it would happen for the deleteItem function, but that method works fine.
To elaborate, if I add 5 items to the list, then add a breakpoint in addItemsAsChild and drop an item on the #1 in the list (to add it as a child), the items state object only has one item in it (it should have 5 since there are 5 items on screen). If I drop an item onto the 2nd item in the list, the item state object has 2 items in it instead of 5... and so on. It seems that the item state gets closed within addItemsAsChild when that item is rendered, but this is only happening for addItemsAsChild and not for the delete?
I cannot figure out why this is happening and several fixes have failed. Can anyone help? Alternative approaches are welcome if you think I'm doing something wrong.
Just figured this out after many wasted hours. react-dnd really need to improve their documentation as this is not an adequate explanation of what the useDrop() hook needs:
depsA dependency array used for memoization. This behaves like the built-in useMemoReact hook. The default value is an empty array for function spec, and an array containing the spec for an object spec.
The translation is that any state objects that will be modified by any callback within useDrop needs to be referenced in the dependency array.
In my DropPane I have a list of components and they appear in the dep array for the useDrop at that level, but in the DroppedItem I have another useDrop.
The solution is the prop-drill the items array all the way down to the DroppedItem component and add the items array as a dependency. I will update the CodeSandbox just for future reference.

How to handle multiple checkboxes in React form?

I have a form that loads preset checkbox selections from the backend. I query the checkbox menu and iterate over it in JSX. Now I want to be able to select/deselect. To handle this, I start out by creating an array of objects matching the size of menu checkbox items like so:
useEffect(() => {
if (!profile) { // this checks if the reusable form is a create or update. If this is an update, I need to prefill an existing array of objects with added key/value pair of checked: true
setConcerns(Array(menuItems.length).fill({ checked: false }))
}
}, [menuItems]) // this happens first
// if it's an update form
useEffect(() => {
if (profile) {
const updatedConcerns = existingConcerns.map((c, i) => ({
...concerns,
c.id: c.id,
checked: true,
}))
}
}, [existingConcerns]) // this happens second
This sets up the concerns array for toggle.
In my JSX I have a Checkbox component:
{menuItems &&
menuItems.map((item: CheckboxProps, index: number) => (
<Checkbox
testID={item?.title}
label={item?.title}
checked={concerns && !!concerns[index]?.checked}
onValueChange={() => handleConcerns(index)}
/>
))}
And the handleConcerns method so far:
const handleConcerns = (index: number) => {
const concernsCopy = [...concerns]
concernsCopy[index].checked = !concernsCopy[index].checked
setConcerns(concernsCopy)
// the rest will determine how to add selected items to concerns
array for submit
}
This causes all of the checkboxes to toggle. This is just the start of pushing menu data into each index of concerns array but first need to get the checkboxes working and be able to have an array of chosen indices to submit to backend.

How to control checkBox's checked props?

I made checkbox component and inside,
set isChecked state to remember checked values even after re-render in case of
new data will be fetched.
but because of this, isChecked state makes every checkbox being checked.
here is example. https://codesandbox.io/s/withered-sun-pm8o9?file=/src/App.js
how can I control checkboxes individually?
Issue
You've only a single isChecked value to toggle all the checkboxes.
A Solution - There are multiple
Store an array of checked booleans. Initialize the state to an array computed from the items prop.
const [isChecked, setIsChecked] = useState(items.map((_) => false));
Update check onChange handler to also consume an index value. Note here that I've also rewritten this handler as a curried function that consumes the item and index and returns the callback function that takes the onChange event object. Use the index to map the previous state to the next state, saving the checked value when the index matches.
const check = (item, index) => (e) => {
const { checked } = e.target;
handleCheck(checked, item);
setIsChecked((isChecked) =>
isChecked.map((el, i) => (i === index ? checked : el))
);
};
When mapping the items prop to the checkbox inputs, use the index to pass to the onChange handler and to access the isChecked checked value.
return items.map((item, i) => (
<FormControlLabel
value="start"
control={
<Checkbox
...
onChange={check(item, i)} // <-- pass item and index i
checked={isChecked[i]} // <-- get checked value from state by index i
/>
}
label={item}
labelPlacement="end"
/>
));
Demo

How do I update an array using useState?

I have the following scenario:
DisplayList component, which renders a list. The list record is passed in via props (this contains the list name, description, author etc), along with the initial items to be listed, and the current 'mode' (can be 'r', 'g' or 'a'). The component displays the list information, and then renders a DisplayItem component for each list item. The DisplayItem component also takes a callback function, so that when the user clicks on an item in the list, it becomes 'tagged' with the current mode (ie the 'mode' property of the item changes).
const DisplayList = (props) => {
// Get list object (contains list name etc)
// and current mode (r, a or g)
const { list, currentMode } = props
// Array of items to show
// This is an array of objects, with each object representing a single item
// eg {name: itemname, mode: r}
const [ items, setItems] = useState(props.items)
// Callback function - triggered when the rendered DisplayItem is clicked
// This changes the 'mode' of the item to the current mode
function handleItemClick(index) {
items[index].mode = currentMode
setItems(items) // At this point the list of items should re-render
}
}
return (
<div className="displayItem">
{
items.map(item => <DisplayItem key={uuid()} item={item} handleCellClick={handleItemClick} />)
}
</div>
)
}
The problem I'm having is that clicking an item seems to trigger the handleItemClick function to update the itemList, but when it gets to setItems, it doesn't re-render.
You are not passing the index to your callback.
try this:
items.map({item, index} => <DisplayItem key={uuid()} item={item} handleCellClick={() => handleItemClick(index)} />)
edited as pointed by Brian Thompson at the comments

Resources