Want to uncheck the node of the tree structure in React JS - reactjs

I have tree structure. What I needed is to uncheck the lover level of the tree when a level higher to it is checked.
But it is not working as I said above. What I have now is shown in the picture below
I clicked TPL
Clicked Accident Report(39)
3 During this time uncheck TPL
Code:
onCheck(checkedKeys) {
const {
FilterTaskList
} = this.props;
console.log(checkedKeys);
var checked2 = [];
if (checkedKeys.checked.length != 0) {
var addedkey = checkedKeys.checked[checkedKeys.checked.length - 1];
checked2 = _.filter(checkedKeys.checked, (o) => o.substring(0, addedkey.length + 1) != addedkey + "-");
}
checkedKeys.checked = checked2;
this.setState({
checkedKeys,
ischecked: true
});
let selectedLevel = 0;
let leveldata = [];
var checked = checkedKeys.checked;
const data = [];
const dataremove = [];
const AllLevel = [];
checked && checked.forEach((value) => {
var keys = value.split("-");
var task;
if (keys.length == 1) {
task = FilterTaskList[keys[0]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': '',
'TLID': task.TLID,
'SelectionLevel': 2,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(2);
}
if (keys.length == 2) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 3,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(3);
}
if (keys.length == 3) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]].Chidrens[keys[2]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 4,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(4);
}
if (keys.length == 4) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]].Chidrens[keys[2]].Chidrens[keys[3]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 5,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(5);
}
The Id's of the tree as follows(backend)(Eg:):
2
2-0
2-0-0

To handle children's state from parent is basically react anti-pattern. You can solve it by registering children refs in parent and touch its checked status via refs. But at the and of the day, I always end up using redux in these cases, it is much cleaner.
Store the whole tree in global state and on firing uncheck action on parent the reducer handle the children's checked state based on parent id.
Meanwhile all the lines are connected components watching their own state based on their ids.
const reducer = (state, action) => {
let isUnchecking;
switch (action.type) {
case 'TOGGLE': return state.map(line => {
if (line.id === action.payload.id) {
isUnchecking = line.checked;
return { ...line, checked: !line.checked }
}
if (line.parent == action.payload.id && isUnchecking) {
return { ...line, checked: false }
}
return line;
});
default: return state;
}
}
const initialState = [
{ id: 1, checked: true, parent: null },
{ id: 2, checked: true, parent: 1 },
{ id: 3, checked: false, parent: 1 },
{ id: 4, checked: false, parent: null }
]
const store = Redux.createStore(reducer, initialState);
const Line = ({ checked, onClick, label, children }) => (
<li onClick={onClick}>
<span>{checked ? '[x] ' : '[_] '}</span>
{label}
<ul>{children}</ul>
</li>
)
const mapStateToProps = state => ({ lines: state })
const LineContainer = ReactRedux.connect(mapStateToProps)(props => {
const { id } = props;
return (
<Line
{...props}
onClick={e => {props.dispatch({ type: 'TOGGLE', payload: { id } }); e.stopPropagation();}}
checked={props.lines.find(line => line.id === id).checked}
/>
)
}
)
class App extends React.Component {
render(){
return (
<ReactRedux.Provider store={store}>
<LineContainer id={1} label="Accident Report">
<LineContainer id={2} label="TPL" />
<LineContainer id={3} label="WC" />
</LineContainer>
<LineContainer id={4} label="Administrative Supervisor" />
</ReactRedux.Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.1/react-redux.js"></script>
<div id="root"></div>

Related

How to update object in array of objects

I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}

useSelector doesnot update the UI

I have a nested state like :
bookingDetails = {
jobCards: [
{
details_id: '1',
parts: [
{...},
{...}
]
}
]}
Now I got the respective jobCards in component from props from parent component i.e detailsID by using useSelector:
const jobCard = useSelector(state => state.bookingDetails.jobCards.find(item => item.details_id === detailsID))
I got a button that successfully adds new object in parts in respective jobCards but that doesnot update the UI.
My bookingDetails Reducer:
case 'ADD_PARTS':
return {
...state,
jobCards: state.jobCards.map(jobCard => {
if (jobCard.details_id === action.id) {
jobCard.parts = [...jobCard.parts, { _id: uuid(), name: '' }]
}
return jobCard
})
}
use like this
const [isJobUpdated, setIsJobUpdated] = useState(false);
const jobCard = useSelector(state => state.bookingDetails.jobCards.find(item => item.details_id === detailsID))
useEffect(() => {
setIsJobUpdated(!!jobCard.length);
}, [jobCard])
return (
<>
{isJobUpdated && <YourComponent />
</>
)
NOTE: this is not the best way to do. You might face re-render issue. Just to check if this solve your current issue.
Forgot to add return statement.
The reducer should have been:
case 'ADD_PARTS':
return {
...state,
jobCards: state.jobCards.map(jobCard => {
if (jobCard.details_id === action.id) {
return {
...jobCard,
parts: [...jobCard.parts, { id: uuid(), name: ''}]
}
}
return jobCard
})
}

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 } )
}

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