Filtering an icon from an array of icon strings for re-render - reactjs

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}

First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Related

React component not re-rendering on component state change

I am working on a sidebar using a recursive function to populate a nested list of navigation items.
Functionally, everything works except for the re-render when I click on one of the list items to toggle the visibility of the child list.
Now, when I expand or collapse the sidebar (the parent component with its visibility managed in its own state), the list items then re-render as they should. This shows me the state is being updated.
I have a feeling this possibly has something to do with the recursive function?
import React, { useState } from "react";
import styles from "./SidebarList.module.css";
function SidebarList(props) {
const { data } = props;
const [visible, setVisible] = useState([]);
const toggleVisibility = (e) => {
let value = e.target.innerHTML;
if (visible.includes(value)) {
setVisible((prev) => {
let index = prev.indexOf(value);
let newArray = prev;
newArray.splice(index, 1);
return newArray;
});
} else {
setVisible((prev) => {
let newArray = prev;
newArray.push(value);
return newArray;
});
}
};
const hasChildren = (item) => {
return Array.isArray(item.techniques) && item.techniques.length > 0;
};
const populateList = (data) => {
return data.map((object) => {
return (
<>
<li
key={object.name}
onClick={(e) => toggleVisibility(e)}
>
{object.name}
</li>
{visible.includes(object.name) ? (
<ul id={object.name}>
{hasChildren(object) && populateList(object.techniques)}
</ul>
) : null}
</>
);
});
};
let list = populateList(data);
return <ul>{list}</ul>;
}
export default SidebarList;
There are many anti patterns with this code but I will just focus on rendering issue. Arrays hold order. Your state does not need to be ordered so it's easier to modify it, for the case of demo I will use object. Your toggle method gets event, but you want to get DOM value. That's not necessary, you could just sent your's data unique key.
See this demo as it fixes the issues I mentioned above.

How to dynamically change object values of a state after a callback in React?

Rookie question.
Consider the code snippet below for a second:
export const Parent = ({data}) => {
const [myData, setShowResults] = useState({isOpen: false, title: 'test'})
function handleCheck(evt) {
console.log(evt.currentTarget.textContent);
//how to change current myData.title with evt.currentTarget.textContent?
}
return (
<div className='c-dropdown'>
<Child data={data} isOpen={myData.isOpen} onCheck={handleCheck}/>
<p>{myData.title}</p>
</div>
);
}
Inside the handleCheck callback function I receive the desired information, yet I cannot find a way how to change myData.title with the newly received information.
Use a functional state update and shallow copy the previous state object.
Example:
function handleCheck(evt) {
const { textContent } = evt.currentTarget;
console.log(textContent);
setShowResults(data => ({
...data, // <-- copy previous state
title: textContent, // <-- update property
}));
}

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

Managing Button State and Resultant Processing with React Hooks

I've got some toggles that can be turned on/off. They get on/off state from a parent functional component. When a user toggles the state, I need to update the state in the parent and run a function.
That function uses the state of all the toggles to filter a list of items in state, which then changes the rendered drawing in a graph visualization component.
Currently, they toggle just fine, but the render gets out of sync with the state of the buttons, because the processing function ends up reading in old state.
I tried using useEffect(), but because the function has a lot of dependencies it causes a loop.
I tried coupling useRef() with useState() in a custom hook to read out the current state of at least the newest filter group that was set, but no luck there either.
Any suggestions on how I could restructure my code in a better way altogether, or a potential solution to this current problem?
Gross function that does the filtering:
function filterItems(threshold, items = {}) {
const { values } = kCoreResult;
const { coloredItems } = rgRef.current;
let itemsForUse;
let filteredItems;
if (Object.entries(items).length === 0 && items.constructor === Object) {
itemsForUse = baseItemsRef.current;
} else {
itemsForUse = items;
}
const isWithinThreshold = id => has(values, id) && values[id] >= threshold;
// filter for nodes meeting the kCoreValue criterion plus all links
filteredItems = pickBy(
itemsForUse,
(item, id) => !isNode(item) || isWithinThreshold(id)
);
filteredItems = pickBy(
filteredItems,
item =>
!has(item, 'data.icon_type') || !filterRef.current[item.data.icon_type]
);
setRg(rg => {
rg.filteredItems = leftMerge(filteredItems, coloredItems);
return {
...rg,
};
});
setMenuData(menuData => {
menuData.threshold = threshold;
return {
...menuData,
};
});
}
Function that calls it after button is pressed that also updates button state (button state is passed down from the filter object):
function changeCheckBox(id, checked) {
setFilter(filter => {
filter[id] = !checked;
return {
...filter,
};
});
filterItems(menuData.threshold);
}
It seems calling your filterItems function in the handler is causing the stale state bug, the state update hasn't been reconciled yet. Separate out your functions that update state and "listen" for updates to state to run the filter function.
Here's a demo that should help see the pattern:
export default function App() {
const [filters, setFilters] = useState(filterOptions);
const onChangeHandler = e => {
setFilters({ ...filters, [e.target.name]: e.target.checked });
};
const filterItems = (threshold, items = {}) => {
console.log("Gross function that does the filtering");
console.log("threshold", threshold);
console.log("items", items);
};
useEffect(() => {
filterItems(42, filters);
}, [filters]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{Object.entries(filters).map(([filter, checked]) => {
return (
<Fragment key={filter}>
<label htmlFor={filter}>{filter}</label>
<input
id={filter}
name={filter}
type="checkbox"
checked={checked}
onChange={onChangeHandler}
/>
</Fragment>
);
})}
</div>
);
}
This works by de-coupling state updates from state side-effects. The handler updates the filters state by always returning a new object with next filter values, and the effect hook triggers on value changes to filters.

Resources