React: Setting state to an array based on textarea input - javascript

I am trying to set the state of features, which is an array, based on inputs into textarea. I am having no luck setting the state of features. When I press one key, that key becomes the next index value in the state. If I hit another key, that key becomes the next index value in the state. So if I manually set my initial state of features to features: ["First", "Second", "Third"],, the result will be features: ["First", "Second", "Third", "Firstf" , "Firstd"] if I hit f and then d
The relevant State
class CreateItem extends Component {
state = {
features: [""],
};
This is how I am attempting to create textarea's based on the state
<label htmlFor="features">
Features
{this.state.features.map((feature, i) => (
<div key={i}>
<textarea
key={i}
type="features"
placeholder={`${feature}`}
name={`features.${i}`}
value={this.state.features[`${i}`]}
onChange={this.handleFeatureArrayChange}
onKeyDown={this.keyPress}
/>
</div>
))}
</label>
Here is the handleChange
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value]
}));
};
This is how I am attempting to create new textarea's each time the user hits enter:
keyPress = e => {
if (e.keyCode == 13) {
this.setState({
features: this.state.features.concat("")
});
}
};

The problem is you're just appending to state each time a key is pressed.
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value] // this is effectively the same as .concat(value)
}));
};
You want to update the value at the current index. Try changing the name of the input to just be key and then mapping instead
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
features: prevState.features.map((feat, key) => {
if (key == name) {
return value
} else
return feat;
}
)
}));
};

Related

Redux helper function to check if a value is in the store

I have a filter panel where the user can select different color filters:
// ColorButton.jsx
function ColorButton({ color }) {
const handleFilterSelected = ({ id }) => {
dispatch(applyFilter({ type: "colors", value: id }));
};
return (
<div className="color-button" onClick={() => handleFilterSelected(color)}>
{isFilterSelected({ type: "colors", value: color.id }) ? "yay" : "nay"}
</div>
);
}
The selected filters are stored in the redux store and the isFilterSelect function looks like this:
// redux/utils/filter.utils.js
export const isFilterSelected = ({ type, value }) => {
const {
filters: { applied }
} = store.getState();
return applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
The issue is that the check runs before a selected filter is added to the applied array.
As a more general question - is it even a good idea to have "helper" functions that depend on the redux store?
Your helper function there should be written as a selector that gets the current state, and you should be using useSelector instead of store.getState manually, as that will update your component when the selector value changes.
function ColorButton({ color }) {
const isSelected = useSelector(state => isFilterSelected(state, { type: "colors", value: color.id }));
return (
<div className="color-button">
{isSelected ? "yay" : "nay"}
</div>
);
}
// redux/utils/filter.utils.js
export const isFilterSelected = (state, { type, value }) => {
return state.filters.applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};

React state changing on change copy of original state?

I am trying to make a simple game like purple pairs in Purple palace game using React. I have a method named clickBtn, it is supposed to increment the count on click and decrement on second click but I don't know why the handleClick method is changing chosen and clicked property of state even if a copy of the new state is made without using setState method. Can you help me fix this?
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: [...generateObList(), ...generateObList()],
count: 0
};
}
handleClick = (id) => {
this.clickBtn(id);
const list = this.state.list.slice();
const current = list.find((a) => a.id === id);
for (let x of list) {
if (x.clicked && x.value == list.find((a) => a.id == id).value) {
x.chosen = true;
x.clicked = false;
current.chosen = true;
current.clicked = false;
// this.setState((prev) => ({
// list: prev.list.map((el) =>
// el.id === id ? current : el.value === current.value ? x : el
// ),
// score: prev.score + 1
// }));
}
}
};
clickBtn = (id) => {
const current = this.state.list.slice().find((e) => e.id === id);
let deClick = current.clicked;
current.clicked = !current.clicked;
console.log(current);
this.setState(
(prev) => ({
list: prev.list.map((el) => (el.id === id ? current : el)),
count: prev.count + (deClick ? -1 : 1)
}),
() => {
console.log(this.state.count, deClick, current);
}
);
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((n) => (
<Card
value={n.value}
onClick={(e) => {
this.handleClick(n.id);
}}
show={!n.chosen}
/>
));
return (
<div class="gameBoard" style={boardStyle}>
{list}
</div>
);
}
}
slice() just return a shallow copy of the array so both original and copy refer to the same array items if the items are reference type ( objects ).
For object slice copies object references into the new array. Both the
original and new array refer to the same object. If a object changes,
the changes are visible to both the new and original arrays.
Try to deep copy the objects. You can easily do deep copy by using, JSON.parse(JSON.stringify(arr))
Also you can try lodash's cloneDeep() method.
For more details about deep cloning - https://stackoverflow.com/a/122704/11306028

How to dynamically fill a state based on textfield input?

I have a form that is rendered using a JavaScript object called model and a text-field that takes in an input. Based on this the state needs to be updated.
Unfortunately the state is updated to the last entry input. And not all the values in the form that have been filled in.
I haven't managed to get the methods mentioned here to work for me: How to fill a specific state with a dynamic key?
constructor(props) {
super(props);
this.state = {
Form:{},
}
}
onChange = (e, key, type) => {
let list = [];
if (type === "text") {
list[key] = e.target.value;
}
else {
// LOGIC TO HANDLE NON TEXT VALUES
let found = this.state.Form[key]? true : false;
if (found) {
list[key] = false;
} else {
list[key]= true;
}
}
this.setState({ Form: list });
};
renderform = () => {
return model.map(m => {
<TextField
type={m.type}
key={m.key}
onChange={event => { this.onChange(event, m.key, "text"); }}
/>
})
}
Try this:
this.setState(prevState => ({ Form: {...prevState.Form, ...list} }));
because when you use this.setState({ Form: list }); your Form state is overwritten with a new array everytime.

How to keep every two identical names red if both were clicked consecutively?

I have list items represent names, when clicking any name it turns red then take one second to return black again, but clicking two identical names consecutively make them keep red color, not turning black again
you can imagine it as a memory game, but i tried to make a simple example here of what i am trying to achieve in the original project
This is my code and my wrong trial:
const App = () => {
const { useState } = React;
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
},
{
name: 'mark',
id: 4,
red: false,
},
{
name: 'peter',
id: 5,
red: false
},
{
name: 'john',
id: 6,
red: false
}
];
const [names, setNames] = useState(items);
const [firstName, setFirstName] = useState(null);
const [secondName, setSecondName] = useState(null)
const handleItemClick = (item) => {
setNames(prev => prev.map(i => i.id === item.id ? { ...i, red: true } : i));
//the problem is here
setTimeout(() => {
setNames(prev => prev.map(n => {
if (secondName && (secondName.name === firstName.name) && n.name === firstName.name) {
return { ...n,red: true }
}
return { ...n, red: false };
}))
}, 1000)
if (!firstName) setFirstName(item);
else if (firstName && !secondName) setSecondName(item)
else if (firstName && secondName) {
setFirstName(item);
setSecondName(null)
}
}
return (
<div class="app">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
handleItemClick={handleItemClick}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { id, name, red } = item;
const { handleItemClick } = props;
return (
<li
className={`${red ? 'red' : ''}`}
onClick={() => handleItemClick(item)}
>
{name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
But this code doesn't work correctly, when clicking two identical names consecutively they don't keep red color and turning black again
To me it seems the issue is overloading the event handler and violating the Single Responsibility Principle.
The handler should be responsible for handling the click event and nothing else. In this case, when the element is clicked you want to add the id to the state of selected/picked names, and toggle the red state value of item with matching id. Factor the timeout effect into (strangely enough) an useEffect hook, with the picks as dependencies. This inverts the logic of the timeout to clearing/resetting the state versus setting what is "red" or not. You can/should also move any logic of determining matches into this same effect (since it already has the dependencies anyway).
useEffect(() => {
... logic to determine matches
const timer = setTimeout(() => {
// time expired, reset only if two names selected
if (firstName && secondName) {
setFirstName(null);
setSecondName(null);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
// clean up old timeout when state updates, i.e. new selected
return () => clearTimeout(timer);
}, [firstName, secondName]);
This will allow you to simplify your name setting logic to
if (!firstName) {
setFirstName(item);
} else {
setSecondName(item);
}
Note: I believe you need another data structure to hold/track/store existing matches made by the user.
How this works:
Starting from clean state, no names are chosen
When first name is picked, firstName is null and updated, red state updated
Timeout is set (but won't clear state yet)
When second name is picked, firstName is defined, so secondName is updated, red state updated
If match, add match to state (to keep red)
Timeout expire and reset state (go back to step 1)
The following is how I'd try to simplify state a bit more, using an array of selected ids that only update if the selected id isn't already chosen and 2 picks haven't been chosen yet.
const App = () => {
const [names, setNames] = useState(items);
const [picks, setPicks] = useState([]);
const [matched, setMatched] = useState({});
/**
* On click event, add id to `picks` array, allow only two picks
*/
const onClickHandler = id => () =>
picks.length !== 2 &&
!picks.includes(id) &&
setPicks(picks => [...picks, id]);
/**
* Effect to toggle red state if id is included in current picks
*/
useEffect(() => {
setNames(names =>
names.map(name => ({
...name,
red: picks.includes(name.id)
}))
);
}, [picks]);
/**
* Effect checks for name match, if a match is found it is added to the
* `matched` array.
*/
useEffect(() => {
// matches example: { mark: 1, peter: 0, john: 0 }
const matches = names.reduce((matches, { name, red }) => {
if (!matches[name]) matches[name] = 0;
red && matches[name]++;
return matches;
}, {});
const match = Object.entries(matches).find(([_, count]) => count === 2);
if (match) {
const [matchedName] = match;
setMatched(matched => ({ ...matched, [matchedName]: matchedName }));
}
const timer = setTimeout(() => {
if (picks.length === 2) {
setPicks([]);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
return () => clearTimeout(timer);
}, [names, picks]);
return (
<div className="App">
<ul>
{names.map(item => (
<Item
key={item.id}
item={item}
matches={matched}
onClick={onClickHandler(item.id)}
/>
))}
</ul>
</div>
);
};
const Item = ({ item, matches, ...props }) => {
const { name, red } = item;
return (
<li
className={classnames({
red: red || matches[name], // for red text color
matched: matches[name] // any other style to make matches stand out
})}
{...props}
>
{name}
</li>
);
};

add selected multiple radio buttons value and uniqueId to an array

I am mapping multiple radio buttons (group) options and when the user click on radio buttons, would like to add selected values and uniqueIds to an array.
with my current code I can get the value that I am currently clicking on but can't add to array.
{result !== null && result.length > 0 ? (
<div>
{result.map(item => {
const label = item.question
const listOptions = item.options.map(item => {
return (
<Radio
name={item.uniqueId}
inline
value={item.uniqueId}
key={item.uniqueId}
className="radio-options"
checked={item.checked}
onChange={e => {
this.handleChange(label, uniqueId);
}}
>
{item.options}
</Radio>
);
});
return (
<div className="radio-options-overview">
<p>{label}</p>
{listOptions}
</div>
);
})}
handleChange = (label, uniqueId) => {
console.log(label, uniqueId);
let addToArray = [];
this.setState({ selectedItems: addToArray})
};
array would look something like this,
[
{ "label": "ageRange", "uniquId": 2 },
{ "label": "smoker", "uniquId": 1 },
{ "label": "exOption", "uniquId": 3 }
]
You are nearly there. #Clarity provided good solution.
if you wanting to replace exisiting value and replace it with new one
Try This
handleChange = (label, uniqueId) => {
const { selectedItems } = this.state
// Find items that already exists in array
const findExistingItem = selectedItems.find((item) => {
return item.uniqueId === uniqueId;
})
if(findExistingItem) {
// Remove found Item
selectedItems.splice(findExistingItem);
// set the state with new values/object
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
} else {
// if new Item is being added
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
}
};
You can do like this:
handleChange = (label, uniqueId) => {
this.setState(state => ({ selectedItems: [...state.selectedItems, uniqueId]}));
};
By using array spread and functional form of setState you make sure that you don't directly mutate the state and add the items to the latest state.
In case you'd want to add an object with a label: uniqueId pair, you could do like so:
handleChange = (label, uniqueId) => {
this.setState(state => ({
selectedItems: [...state.selectedItems, {
[label]: uniqueId
}]
}));
};
EDIT: If you want to overwrite the items with the same labels, the easiest would be to store them as an object and not an array, so the item with the same label would overwrite an existing one:
handleChange = (label, uniqueId) => {
this.setState(state => {
return { selectedItems: { ...state.selectedItems, [label]: uniqueId } };
});
};
Honestly, I don't understand, what are you trying to do, but if you need to add object (or something else) inside an array, you could use .push() method. For example:
let addToArray = [];
let test = {"label": "ageRange", "uniquId": 2};
addToArray.push(test);
console.log(addToArray); //[{label: "ageRange", uniquId: 2}]

Categories

Resources