How do I update an array using useState? - reactjs

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

Related

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.

ReactJS sort by Asc/Desc not rendering sorted list from nested component

Project Overview
I am currently learning ReactJS and am creating a Pokedex app that allows the user to sort Pokemon objects by specific properties (name, type, hp, etc). The end product should have a search input to filter by pokemon name, a drop-down selector for sort criteria, and buttons for sort Ascend and sort Descend. State being tracked is searchQuery, sortBy, sortSelected, and pokeData(my data file).
Full code can be viewed here: https://github.com/julianne-vela/Pokedex-React/tree/dev
File Structure: SearchPage.js > SideBar.js > SortMenu.js
Problem
onClick button in SortMenu is not rendering sorted Pokemon list.
Expected Result: Click Asc/Desc button in SortMenu -> Pokemon list renders in sorted order based on criteria selected in drop-down and Asc/Desc button clicked.
Actual Result: Drop-down is updating state with correct sort criteria selected but button is not triggering any action when clicked.
What I've tried
Using destructured props throughout project
const {
sortBy,
sortSelected,
} = this.state
Currently passing the following props from SearchPage.js to child SideBar.js:
sortByValues={sortBy} // Array of sort criteria options stored in state
sortSelected={sortSelected} // Currently selected sort criteria from drop-down
handleSortSelected={this.handleSortSelected} // event handler to update state with currently selected sort criteria
sortAsc={this.sortAsc} // function to trigger Asc sort onClick
sortDesc={this.sortDesc} // function to trigger Asc sort onClick
SortMenu.js Code
import React, { Component } from 'react';
export default class SortMenu extends Component {
render() {
const {
sortByValues,
handleSortSelected,
sortAsc,
sortDesc,
} = this.props
const options = sortByValues.map(option => <option value={option} key={option}>{option}</option>)
return (
<aside>
{/* DropDown Sort By */}
<select className='dropdown'
onChange={handleSortSelected}>
{options}
</select>
{/* Sort Ascending/Descending Buttons */}
<button className='sortBtn' value='ascending' onClick={sortAsc}>Ascending</button>
<button className='sortBtn' value='descending' onClick={sortDesc}>Descending</button>
</aside >
)
}
}
I feel it's important to note that we are using strictly class components and therefore are not using constructor(props) in this project. Instead, I am using arrow functions throughout to implicitly bind this where needed.
To reiterate my goal: I need to dynamically render the pokemon list based on the sort criteria selected in the drop-down list as well as the ascend/descend button clicked. The list should update onClick.
I have been working on this for 3 days and have gone through so many iterations of sort functions that I can't even see straight anymore. I'm thinking this might be a simple over-sight due to the sheer amount of code I'm writing as well as the fact that this is a new language (I'm already proficient in Vanilla JS).
Any guidance here would be GREATLY appreciated as I'm really at a wall at this point. I don't know what else to try in order to render the sorted list.
Update
Also, would it be easier if I were to create a toggle button instead of two separate buttons for Asc/Desc?
Thank you!
Update 2
Currently I am rendering my pokemon objects in a module component that I'm calling within the SearchPage.js component. Here is the code for the Pokemon List:
export default class PokemonList extends Component {
render() {
const { filteredPokemon } = this.props
return (
<content className='pokemon-list float'>
{filteredPokemon.map(pokeObject =>
<PokeItem
key={pokeObject._id}
pokeImage={pokeObject.url_image}
pokeName={capFirstLetter(pokeObject.pokemon)}
pokeType={capFirstLetter(pokeObject.type_1)}
pokeHp={pokeObject.hp}
pokeAtt={pokeObject.attack}
pokeDef={pokeObject.defense}
/>)}
</content>
);
}
}
This is pulling the filtered pokemon from my filter method on SearchPage.js:
const filteredList = pokeData.filter(pokeObject => {
return pokeObject['pokemon'].includes(this.state.searchQuery) || pokeObject['type_1'].includes(tFilterSelected);
});
To sort the items, I'm using two separate arrow functions. These are housed in the first section of the SearchPage.js component (outside of render and return):
sortAsc = () => {
this.setState(prevState => {
this.state.pokeData.sort((a, b) => (a[this.state.sortSelected] - b[this.state.sortSelected]))
})
}
sortDesc = () => {
this.setState(prevState => {
this.state.pokeData.sort((a, b) => (b[this.state.sortSelected] - a[this.state.sortSelected]))
})
}
Update 3
Added project to CodeSandbox. Can view here: https://codesandbox.io/s/pokedex-react-crflb
A comparator function that subtracts the arguments would work fine for sorting numbers but would fail on strings since it would return NaN.
sortAsc = () => {
const { pokeData, sortSelected } = this.state
this.setState({
pokeData: [...pokeData].sort((a, b) => {
if (a[sortSelected] > b[sortSelected]) {
return 1
}
if (a[sortSelected] < b[sortSelected]) {
return -1
}
return 0
})
})
}
sortDesc = () => {
const { pokeData, sortSelected } = this.state
this.setState({
pokeData: [...pokeData].sort((a, b) => {
if (a[sortSelected] > b[sortSelected]) {
return -1
}
if (a[sortSelected] < b[sortSelected]) {
return 1
}
return 0
})
})
}
Working CodeSandbox

Customize Array State in ReactJs with ES6

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.

I'm passing the 'item' parent/children and when I manipulate it changes the default value in parent

need help. I got a Card component that executes a function passing 'item' and setting insides a State. And my Modal component receive that 'item' and render on screen. Besides, when I manipulate on modal component the items values it changes the default value on Card component.
# this is my HomeScreen and the function that sets the item value.
const handleModalVisibleProduct = (item) => {
setModalData(item)
setModalVisibleProduct(!modalVisibleProduct)
}
Then I pass to ProductsModal item
<ProductModal
products={products}
modalData={modalData}
modalVisible={modalVisibleProduct}
handleCloseVisibleProduct={handleCloseVisibleProduct}
handleAddCart={handleAddCart}
/>
Yes it is because it uses the same reference of the item's object. You would have to change it's reference to get the required result like
const handleModalVisibleProduct = (item) => {
setModalData( {...item} );
....
}

Unable to display output in React Hooks

I have a list of items and on button click it should display more information on that particular item. For this, I have created one hook that gets the list of all the items from API and displays the list as it iterates inside a <ul> tag. (this is inside the return tag.) On each iteration it shows a button called "Details"
<button onClick ={moreInfo.bind(this, post)} >Details</button>
and when that is clicked it sends the information on that object from the list to the following code -
const moreInfo = (index) => {
console.log(index.name) //works
return ( <div>
{index.map = ( x => (
<div>{x.name}</div>
))}
</div>) //no output
}
console.log is displaying the name but nothing displays via return tag here
You are not mapping thought the array, you are just assigning map property to the index variable
Try do that, like this:
const MoreInfo = (index) => {
return (
<div>
{index.map(x => <div>{x.name}</div>)}
</div>
)
}

Resources