Handle dynamic select components in state - reactjs

I am trying to handle dynamic React Select components that are coming from props and set their values into state and handle their change event to update the state. My code works but i am wondering if this is the correct approach in function updateItem to do this. I am pasting my component code below.
export default class Test extends Component {
constructor(props) {
super(props);
this.state = {
filters:[],
};
this.handleFilterUpdate = this.handleFilterUpdate.bind(this)
this.updateItem = this.updateItem.bind(this)
}
updateItem(id, itemAttributes) {
const index = this.state.filters.findIndex(x=> x.key === id);
if (index === -1) {
this.setState( {filters: [...this.state.filters, {key: id, value: itemAttributes.value}]})
} else {
this.setState({
filters: [
...this.state.filters.slice(0, index),
Object.assign({}, this.state.filters[index], {key: id, value: itemAttributes.value}),
...this.state.filters.slice(index + 1)
]
});
}
}
handleFilterUpdate(control,obj){
this.updateItem(control, obj)
}
renderFilters(settings, controls){
return controls.map((control) => (
<Select
id={control.key}
name={control.name}
options={control.choices}
clearable={false}
onChange={this.handleFilterUpdate.bind(this, control.key)}
/>
));
}
render() {
return (
{this.renderFilters(this.state.filters, this.props.filters)}
)
}
}

It is better to use setState(oldState => newState) when new state is based on old values:
updateItem(id, itemAttributes) {
this.setState(oldState => {
const index = oldState.filters.findIndex(x=> x.key === id);
if (index === -1) {
return {filters: [...oldState.filters, {key: id, value: itemAttributes.value}]};
} else {
return {
filters: [
...oldState.filters.slice(0, index),
Object.assign({}, oldState.filters[index], {key: id, value: itemAttributes.value}),
...oldState.filters.slice(index + 1)
]
};
}
});
}

Related

React - update nested state (Class Component)

I'm trying to update a nested state. See below. The problem is that upon clicking on a category checkbox, instead of updating the {categories: ....} object in state, it creates a new object in state:
class AppBC extends React.Component {
constructor(props) {
super(props)
this.state = {
products: [],
categories: []
}
this.handleSelectCategory = this.handleSelectCategory.bind(this);
}
componentDidMount() {
this.setState({
products: data_products,
categories: data_categories.map(category => ({
...category,
selected: true
}))
});
}
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState.categories.map(
category => {
if(category.id === id){
return {
...category,
selected: !category.selected,
}
}else{
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
} // handleSelectCategory
render() {
return(
<div className="bc">
<h1>Bare Class Component</h1>
<div className="main-area">
<Products categories={this.state.categories} products={this.state.products} />
<Categories
categories={this.state.categories}
handleSelectCategory={this.handleSelectCategory}
/>
</div>
</div>
);
};
Initial state before clicking (all categories are selected):
After clicking on an a checkbox to select a particular category, it saves a new object to state (correctly reflecting the category selection) instead of updating the already existin categories property:
Change your update to:
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState,
categories: prevstate.categories.map(
category => {
if (category.id === id) {
return {
...category,
selected: !category.selected,
}
} else {
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
}
I prefer this way, it's more easy for reading
handleSelectCategory(id) {
const index = this.state.categories.findIndex(c => c.id === id);
const categories = [...this.state.categories];
categories[index].selected = !categories[index].selected;
this.setState({ categories });
}
If your purpose is to only change selected property on handleSelectCategory function,
Then you could just do it like
run findIndex on array and obtain index for id match from array of objects.
update selected property for that index
Code:
handleSelectCategory(id) {
let targetIndex = this.state.categories.findIndex((i) => i.id === id);
let updatedCategories = [...this.state.categories];
if (targetIndex !== -1) {
// this means there is a match
updatedCategories[targetIndex].selected = !updatedCategories[targetIndex].selected;
this.setState({
categories: updatedCategories,
});
} else {
// avoid any operation here if there is no "id" matched
}
}

React js Change Field specifying the attribute name

I have attributes in the state, I would like to ensure that by specifying the function the attribute name changes the value contained in the state.
It seems to work, the problem that if I have an object of this type in the state:
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
It does not work properly, as the code is now set in the state in this case a new attribute is created.
So I'd like to do something like this:
handleChangeField("companyInfo.name")
It is changed to the state atrribute name of the obj companyInfo that is in the state.
Can you give me some advice?
Link: codesandbox
Code:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import ReactJson from "react-json-view";
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
email: "email0",
role: "role0",
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
};
}
returnStateElement = (...elements) => {
const copy = Object.assign({}, this.state);
return elements.reduce((obj, key) => ({ ...obj, [key]: copy[key] }), {});
};
handleChangeField = field => evt => {
let state = {};
state[field] = evt.target.value;
this.setState(state);
};
handleSubmit = () => {
let el = this.returnStateElement(
"name",
"email",
"vatNumber",
"legalRepresentative",
"role"
);
let { name, email, legalRepresentative, vatNumber, role } = el;
let dataSender = {};
dataSender.email = email;
dataSender.role = role;
dataSender.companyInfo = {};
dataSender.companyInfo.name = name;
dataSender.companyInfo.legalRepresentative = legalRepresentative;
dataSender.companyInfo.vatNumber = vatNumber;
console.log(this.state);
//console.log(dataSender)
};
render() {
return (
<div>
<input onChange={this.handleChangeField("email")} />
<br />
<br />
<input onChange={this.handleChangeField("companyInfo.name")} />
<br />
<br />
<button onClick={() => this.handleSubmit()}>send</button>
<br />
<br />
<ReactJson src={this.state} theme="solarized" />
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
Edit: I came up with a much better answer where one mutates the specific key of the oldState using a reduce. Less code, much more elegant and should work at any object depth.
Working example here
setNestedField(object, fields, newValue) {
fields.reduce((acc, field, index) => {
if (index === fields.length - 1) {
acc[field] = newValue;
}
return acc[field];
}, object);
return object;
}
handleChangeField = field => evt => {
const fields = field.split(".");
let oldState = this.state;
const newState = this.setNestedField(
{ ...oldState },
fields,
evt.target.value
);
this.setState(newState);
};
OLD ANSWER
handleChangeFields looks like this:
handleChangeField = field => evt => {
//first you split by '.' to get all the keys
const fields = field.split(".").reverse();
// you'll need the previous state
let oldState = this.state;
let newState = fields.reduce((acc, value, index) => {
if (index === 0) {
// you add the event value to the first key
acc[value] = evt.target.value;
return acc;
}
//copy acc to use it later
const tmp = { ...acc };
//delete previous key added to acc
delete acc[fields[index - 1]];
acc[value] = { ...oldState[value], ...tmp };
return acc;
}, {});
this.setState(newState);
};
What's going on step by step in the reduce function, if you do handleChangeField('company.name') with evt.target.value = "Big Corp":
1) you get the array ['name','company']
2) you go in the reduce function
when index = 0, acc = {}, key='name' => {name: 'Big Corp'}
when index=1, acc= {name: 'Big Corp'},key='company' => acc = { company: {name: 'Big Corp'}, name: 'BigCorp} so before returning we delete the previous key (name here) to return => { company: {name: 'Big Corp'}

What is the best way to update object array value in React

My React state:
//...
this.state = {
mylist: [
{
"id": 0,
"trueorfalse": false
},
{
"id": 1,
"trueorfalse": false
}
]
}
//...
I am trying to update the trueorfalse value based on the id
Here is what I did so far but didn't work:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
var TorF = true
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { TorF }) : el))
})
}
I really want to make it dynamic so the trueorfase will be opposite of what it is now:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { /* if already true set to false or vice versa */ }) : el))
})
}
How can I update my code to have the dynamicity shown in the second example (if possible), otherwise the first example would do just fine
Another solution using map:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mylist: [
{
id: 0,
trueorfalse: false
},
{
id: 1,
trueorfalse: true
}
]
};
}
toggleBoolean = () => {
const ID = Number(this.state.selectedID);
this.setState(prevState => ({
mylist: prevState.mylist.map(item => {
if (item.id === ID) {
return { ...item, trueorfalse: !item.trueorfalse };
} else {
return item;
}
})
}));
};
render() {
return (
<div className="App">
<p>{`State values: ${JSON.stringify(this.state.mylist)}`}</p>
<button onClick={this.toggleBoolean}>Change true/false values</button>
<label>Insert ID:</label>
<input
type="number"
onChange={event => this.setState({ selectedID: event.target.value })}
/>
</div>
);
}
}
I think the following code would accomplish your second question.
var idnum = e.target.id.toString().split("_")[1]
let newList = Array.from(this.state.mylist) //create new array so we don't modify state directly
if (type === 1) {
let objToUpdate = newList.find((el) => el.id === idnum) // grab first element with matching id
objToUpdate.trueorfalse = !objToUpdate.trueorfalse
this.setState( { mylist: newList } )
}

How to safely update my state when I have to traverse and lookup/remove items in my state

I need to modify my state and I am unsure how to do it correctly.
My account property in my state looks something like this:
{
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54
}
]
},
{
"id":9,
"products":[
{
"productId":89
}
]
}
]
}
}
My action dispatches the following:
dispatch({
type: Constants.MOVE_PRODUCT,
productId: 54,
sourceCategoryId: 7,
targetCategoryId: 9
});
Now my reducer skeleton is:
const initialState = {
account: null,
};
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
/*
action.productId
action.sourceCategoryId
action.targetCategoryId
*/
const sourceCategoryIndex = state.account.categories.findIndex((category) => { return category.id === action.sourceCategoryId; });
const sourceCategory = state.account.categories[sourceCategoryIndex];
const targetCategoryIndex = state.account.categories.findIndex((category) => { return category.id === action.targetCategoryId; });
const targetCategory = state.account.categories[targetCategoryIndex];
// ??
return {...state};
}
}
export default accounts;
I am confused, if I update the state directly inside of the switch block, is that wrong?
Does it have to be a one-liner update that does the mutation in-place or as long as I do it in the switch block it is fine?
Update
From the action, I need to remove the productId from the sourceCategoryId and add it to the targetCategoryId inside of the account state object.
Yes, you should not be doing state.foo = 'bar' in your reducer. From the redux docs:
We don't mutate the state. We create a copy with Object.assign(). Object.assign(state, { visibilityFilter: action.filter }) is also wrong: it will mutate the first argument. You must supply an empty object as the first parameter. You can also enable the object spread operator proposal to write { ...state, ...newState } instead.
So your reducer could look like
function accountsReducer (state = initialState, { sourceCategoryId, productId }) {
const targetProduct = state.categories
.find(({ id }) => id === sourceCategoryId)
.products
.find(({ id }) => id === productId);
switch (action.type) {
case Constants.MOVE_PRODUCT:
return {
...state,
categories: state.categories.reduce((acc, cat) => {
return cat.id !== sourceCategoryId
? {
...acc,
cat: { ...cat, products: cat.products.filter(({ id }) => id !== productId) }
}
: {
...acc,
cat: { ...cat, products: [...cat.products, targetProduct] }
}
}, {});
};
}
}
But this a pain...you should try to normalize your data into a flat array.
// first, let's clean up the action a bit
// type and "payload". I like the data wrapped up in a bundle with a nice
// bow on it. ;) If you don't like this, just adjust the code below.
dispatch({
type: Constants.MOVE_PRODUCT,
payload: {
product: { productId: 54 }
sourceCategoryId: 7,
targetCategoryId: 9
}
});
// destructure to get our id and categories from state
const { id, categories } = state
// map the old categories to a new array
const adjustedCategories = categories.map(cat => {
// destructure from our payload
const { product, sourceCategoryId, targetCategoryId } = action.payload
// if the category is the "moving from" category, filter out the product
if (cat.id === sourceCategoryId) {
return { id: cat.id, products: [...cat.products.filter(p => p.productId !== product.productId)
}
// if the category is our "moving to" category, use the spread operator and add the product to the new array
if (cat.id === targetCategoryId) {
return { id: cat.id, products: [...cat.products, product] }
}
)
// construct our new state
return { id, categories: adjustedCategories }
This solution keeps the function pure and should give you what you want. It's not tested, so may not be perfect.
You could take the following approach:
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
// Extract action parameters
const { productId, sourceCategoryId, targetCategoryId } = action
// Manually "deep clone" account state
const account = {
id : state.account.id,
categories : state.account.categories.map(category => ({
id : category.id,
products : category.products.map(product => ({ productId : product.productId })
}))
}
// Extract source and target categories
const sourceCategory = account.categories.find(category => category.id === sourceCategoryId);
const targetCategory = account.categories.find(category => category.id === targetCategoryId);
if(sourceCategory && targetCategory) {
// Find product index
const index = sourceCategory.products.findIndex(product => (product.productId === action.productId))
if(index !== -1) {
const product = sourceCategory.products[index]
// Remove product from source category
sourceCategory.products.splice(index, 1)
// Add product to target category
targetCategory.products.splice(index, 0, product)
}
}
return { account };
}
}
Here is the ugly solution :)
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
const sourceCategoryIndex = state.account.categories.findIndex(
el => el.id === action.sourceCategoryId
);
const targetCategoryIndex = state.account.categories.findIndex(
el => el.id === action.targetCategoryId
);
const sourceCategory = state.account.categories.find(
el => el.id === action.sourceCategoryId
);
const targetCategory = state.account.categories.find(
el => el.id === action.targetCategoryId
);
const itemToMove = sourceCategory.products.find(
el => el.productId === action.productId
);
const newSourceCategory = {
...sourceCategory,
products: sourceCategory.products.filter(
el => el.productId !== action.productId
)
};
const newTargetCategory = {
...targetCategory,
products: [...targetCategory.products, itemToMove]
};
const newCategories = Object.assign([], state.account.categories, {
[sourceCategoryIndex]: newSourceCategory,
[targetCategoryIndex]: newTargetCategory
});
return { ...state, account: { ...state.account, categories: newCategories } };
}
};
Phew :) As a learner it's quite good for me :) But, I like #Daniel Lizik's approach, using reduce.
Here is the working example:
const action = {
productId: 54,
sourceCategoryId: 7,
targetCategoryId: 9,
}
const state = {
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54,
},
{
"productId":67,
},
]
},
{
"id":9,
"products":[
{
"productId":89,
}
]
}
]
}
};
const sourceCategoryIndex = state.account.categories.findIndex( el => el.id === action.sourceCategoryId );
const targetCategoryIndex = state.account.categories.findIndex( el => el.id === action.targetCategoryId );
const sourceCategory = state.account.categories.find( el => el.id === action.sourceCategoryId );
const targetCategory = state.account.categories.find( el => el.id === action.targetCategoryId );
const itemToMove = sourceCategory.products.find( el => el.productId === action.productId );
const newSourceCategory = {...sourceCategory, products: sourceCategory.products.filter( el => el.productId !== action.productId ) };
const newTargetCategory = { ...targetCategory, products: [ ...targetCategory.products, itemToMove ] };
const newCategories = Object.assign([], state.account.categories, { [sourceCategoryIndex]: newSourceCategory,
[targetCategoryIndex]: newTargetCategory }
);
const newState = { ...state, account: { ...state.account, categories: newCategories } };
console.log( newState );

ReactJS seems combine two state updates as one render, how to see separate rendering effects?

I am trying to come up a react exercise for the flip-match cards game: say 12 pairs of cards hide (face down) randomly in a 4x6 matrix, player click one-by-one to reveal the cards, when 2 cards clicked are match then the pair is found, other wise hide both again., gane over when all pairs are found.
let stored = Array(I * J).fill(null).map((e, i) => (i + 1) % (I * J));
/* and: randomize (I * J / 2) pairs position in stored */
class Board extends React.Component {
constructor() {
super();
this.state = {
cards: Array(I*J).fill(null),
nClicked: 0,
preClicked: null,
clicked: null,
};
}
handleClick(i) {
if (!this.state.cards[i]) {
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[i] = stored[i];
return {
cards: upCards,
nClicked: prevState.nClicked + 1,
preClicked: prevState.clicked,
clicked: i,
};
}, this.resetState);
}
}
resetState() {
const preClicked = this.state.preClicked;
const clicked = this.state.clicked;
const isEven = (this.state.nClicked-1) % 2;
const matched = (stored[preClicked] === stored[clicked]);
if (isEven && preClicked && clicked && matched) {
// this.forceUpdate(); /* no effects */
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[preClicked] = null;
upCards[clicked] = null;
return {
cards: upCards,
nClicked: prevState.nClicked,
preClicked: null,
clicked: null,
};
});
}
}
renderCard(i) {
return <Card key={i.toString()} value={this.state.cards[i]} onClick={() => this.handleClick(i)} />;
}
render() {
const status = 'Cards: '+ I + ' x ' + J +', # of clicked: ' + this.state.nClicked;
const cardArray = Array(I).fill(null).map(x => Array(J).fill(null));
return (
<div>
<div className="status">{status}</div>
{ cardArray.map((element_i, index_i) => (
<div key={'row'+index_i.toString()} className="board-row">
{ element_i.map((element_j, index_j) => this.renderCard(index_i*J+index_j))
}
</div>
))
}
</div>
);
}
}
Essentially, Board constructor initialize the state, and handleClick() calls setState() to update the state so it trigger the render of the clicked card's value; the callback function resetState() is that if the revealed two card did not match, then another setState() to hide both.
The problem is, the 2nd clicked card value did not show before it goes to hide. Is this due to React combine the 2 setState renderings in one, or is it rendering so fast that we can not see the first rendering effects before the card goes hide? How to solve this problem?
You're passing resetState as the callback to setState, so I would expect after the initial click your state will be reset.
You might want to simplify a bit and do something like this:
const CARDS = [
{ index: 0, name: 'Card One', matchId: 'match1' },
{ index: 1, name: 'Card Two', matchId: 'match2' },
{ index: 2, name: 'Card Three', matchId: 'match1', },
{ index: 3, name: 'Card Four', 'matchId': 'match2' },
];
class BoardSim extends React.Component {
constructor(props) {
super(props);
this.state = {
cardsInPlay: CARDS,
selectedCards: [],
checkMatch: false,
updateCards: false
};
...
}
...
componentDidUpdate(prevProps, prevState) {
if (!prevState.checkMatch && this.state.checkMatch) {
this.checkMatch();
}
if (!prevState.updateCards && this.state.updateCards) {
setTimeout(() => {
this.mounted && this.updateCards();
}, 1000);
}
}
handleCardClick(card) {
if (this.state.checkMatch) {
return;
}
if (this.state.selectedCards.length === 1) {
this.setState({ checkMatch: true });
}
this.setState({
selectedCards: this.state.selectedCards.concat([card])
});
}
checkMatch() {
if (this.selectedCardsMatch()) {
...
}
else {
...
}
setTimeout(() => {
this.mounted && this.setState({ updateCards: true });
}, 2000);
}
selectedCardsMatch() {
return this.state.selectedCards[0].matchId ===
this.state.selectedCards[1].matchId;
}
updateCards() {
let cardsInPlay = this.state.cardsInPlay;
let [ card1, card2 ] = this.state.selectedCards;
if (this.selectedCardsMatch()) {
cardsInPlay = cardsInPlay.filter((card) => {
return card.id !== card1.id && card.id !== card2.id;
});
}
this.setState({
selectedCards: [],
cardsInPlay,
updateCards: false,
checkMatch: false
});
}
render() {
return (
<div>
{this.renderCards()}
</div>
);
}
renderCards() {
return this.state.cardsInPlay.map((card) => {
return (
<div key={card.name} onClick={() => this.handleCardClick(card)}>
{card.name}
</div>
);
});
}
...
}
I've created a fiddle for this you can check out here: https://jsfiddle.net/andrewgrewell/69z2wepo/82425/

Resources