setState array of objects without changing objects order - javascript

In state, I have an array of list objects and would like to toggle the displayAddTaskForm. Every time I set state, the list that gets clicked gets rearranged and moves to the front of the lists on the UI. I believe I know why this is happening, just not sure of the solution. When I setState in toggleAddTaskForm, toggledList is first and then the other lists. How do I update the toggledList without changing the order of the lists. Here is the code.
this.state = {
lists: [
{
id: 1,
header: "To Do",
displayAddTaskForm: false,
},
{
id: 2,
header: "Working on It",
displayAddTaskForm: false,
},
{
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false,
}
],
toggleAddTaskForm: (list) => {
const toggledList = {...list, displayAddTaskForm: !list.displayAddTaskForm}
const otherLists = this.state.lists.filter(l => l.id !== list.id)
this.setState({
lists: [toggledList, ...otherLists]
})
},
}

Putting function in the state is not a common thing. I think you need to seperate that function from state.
You can easily toggle items using Array.map() and checking the clicked item id with the item id. This will not change the order of items.
You can use the following code to toggle only one item:
class App extends Component {
constructor() {
super();
this.state = {
lists: [
{
id: 1,
header: "To Do",
displayAddTaskForm: false
},
{
id: 2,
header: "Working on It",
displayAddTaskForm: false
},
{
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false
}
]
};
}
handleClick = id => {
let newList = this.state.lists.map(item => {
if (item.id === id) {
return {
...item,
displayAddTaskForm: !item.displayAddTaskForm
};
} else {
return {
...item,
displayAddTaskForm: false
};
}
});
this.setState({ lists: newList });
};
render() {
return (
<div>
<ul>
{this.state.lists.map(({ id, header, displayAddTaskForm }) => {
return (
<li key={id} onClick={() => this.handleClick(id)}>
{header} - Toggle Value: {displayAddTaskForm ? "true" : "false"}
</li>
);
})}
</ul>
</div>
);
}
}
Playground
Or if you want to be able to toggle every item, you can change the handleClick function like this:
handleClick = id => {
let newList = this.state.lists.map(item => {
if (item.id === id) {
return {
...item,
displayAddTaskForm: !item.displayAddTaskForm
};
} else {
return {
...item
};
}
});
this.setState({ lists: newList });
};
Playground

You could find the index of the list, copy the lists, and insert the modified list into the new lists array at the same index.
toggleAddTaskForm: (list) => {
const toggledList = {...list, displayAddTaskForm: !list.displayAddTaskForm}
const newLists = [...this.state.lists];
newLists[this.state.lists.indexOf(list)] = toggledList;
this.setState({
lists: newLists
})
}

This might help.
lists = [{
id: 1,
header: "To Do",
displayAddTaskForm: false,
}, {
id: 2,
header: "Working on It",
displayAddTaskForm: false,
}, {
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false,
}]
const toggle = (list)=>{
const toggledList = {
...list,
displayAddTaskForm: true
}
const indexOfList = lists.findIndex(({id}) => id === list.id)
const newLists = [...lists]
newLists[indexOfList] = toggledList
setState({
lists: newLists
})
}
const setState = (o) => console.log(o)
toggle(lists[1])

Related

How to manage props of a list of components in React?

I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;

State updates are unclear and not rendering my catalog of products based on menu selection

I'm updating two states where setProductsShow depends on setSelectedCategories. setSelectedCategories is updated first then setProductsShow is updated to render products.
There are plenty of solutions on React state updates and I've read other answers on the matter but I'm confused on the order of which the states update for my specific case.
products
[
{
"name": "Potato",
"category": "food"
},
{
"name": "iPhone",
"category": "tech"
}
]
menuItems
[
{
"name": "tech",
"menuSelect": false
},
{
"name": "food",
"menuSelect": false
}
]
const [productsShow, setProductsShow] = useState(products)
const [selectedCategories, setSelectedCategories] = useState(menuItems) // Default all items selected
// Event is triggered by a change in menu selection; code not shown
function handleChangeMenu(event) {
let { name, checked } = event.target
// Update categories based on menu selection
setSelectedCategories(category => (
category.map(item => item.name == name ? {
...item,
menuSelect: checked
} : item)
))
// If category is selected, then push onto an array
let flatSelected = []
setSelectedCategories(c => {
flatSelected = c.map(selected => {
if (selected.menuSelect) {
flatSelected.push(selected.name)
}
})
return c // Return the state since we're only reading
})
console.log("FLATSELECTED")
console.log(flatSelected)
// Render products which are marked as TRUE in array of menu items "flatSelected[]"
setProductsShow(() => {
console.log("INSIDE SETPRODUCTSSHOW")
console.log(products.filter(product => flatSelected.includes(product.category)))
console.log("FLATSELECTED")
console.log(flatSelected)
products.filter(product => flatSelected.includes(product.category))
})
}
The outcome I get is console.log(products.filter(product => flatSelected.includes(product.category))) prints out an empty array.
The problem is that you are setting state and try to access the state before the component has been rerenderd. The second time you call setSelectedCategories categories hasn't had time to get updated.
Set state at the end of the function and it should work:
import { useState } from "react";
const products = [
{
name: "Potato",
category: "food",
},
{
name: "iPhone",
category: "tech",
},
];
const menuItems = [
{
name: "tech",
menuSelect: false,
},
{
name: "food",
menuSelect: false,
},
];
function App() {
const [productsShow, setProductsShow] = useState(products);
const [selectedCategories, setSelectedCategories] = useState(menuItems);
function handleChangeMenu(event) {
//mock the event
let { name, checked } = { name: "tech", checked: true };
//updating catagory
const newSelectedCategories = menuItems.map((category) =>
category.name === name ? { ...category, menuSelect: checked } : category
);
//updating products
const newProductsShow = products.filter((product) =>
newSelectedCategories.some(
({ name, menuSelect }) => name === product.category && menuSelect
)
);
// Update state based on menu selection
setSelectedCategories(newSelectedCategories);
setProductsShow(newProductsShow);
}
return (
<div>
<button onClick={handleChangeMenu}> Toogle tech </button>
{productsShow.map((product) => (
<div> {product.name}</div>
))}
</div>
);
}
export default App;

Change the state of an array of checkboxes on the reducer as it changes in the UI

I have something like this on React:
const CheckboxItems = (t) => [ // that t is just a global prop
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
labelText: t('cancellations.checkBoxItemsCancelled'),
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
labelText: t('cancellations.checkboxRequestDate'),
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
labelText: t('cancellations.checkboxStatus'),
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
labelText: t('cancellations.checkboxRequestedBy'),
},
];
class TableToolbarComp extends React.Component {
state = {
items: CheckboxItems(),
};
onChange = (value, id, event) => {
const { columnsFilterHandler } = this.props;
this.setState(({ items }) => {
const item = items.slice().find(i => i.id === id);
if (item) {
item.checked = !item.checked;
columnsFilterHandler(id, item.value, item.checked);
return { items };
}
});
};
render() {
const { items } = this.state;
return(
<>
{items.map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={item.checked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
</>
)
}
export default compose(
connect(
({ cancellations }) => ({
columnId: cancellations.columnId,
columnValue: cancellations.columnValue,
isChecked: cancellations.isChecked,
}),
dispatch => ({
columnsFilterHandler: (columnId, columnValue, isChecked) => {
dispatch(columnsFilterAction(columnId, columnValue, isChecked));
},
}),
),
)(translate()(TableToolbarComp));
That works very well and it is dispatching the data I would need to use later.
But I have a mess on the Redux part which is changing the state of all of the checkboxes at once and not separately as it should. So, once I uncheck one of the checkboxes the other 3 also get checked: false. I don't see this change to checked: false on the UI, only I see it on the Redux console in the browser.
This is what I have in the reducer
const initialState = {
checkboxes: [
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
},
],
}
[ActionTypes.COLUMNS_FILTER](state, action) {
return initialState.checkboxes.map(checkbox => {
if (!checkbox.id === action.payload.id) {
return checkbox;
}
return {
...checkbox,
checked: action.payload.isChecked,
};
});
}
Action:
const columnsFilterAction = (columnId, columnValue, isChecked) => ({
type: ActionTypes.COLUMNS_FILTER,
payload: { columnId, columnValue, isChecked },
});
So all I need to know is what I have to do manage the state of those checkboxes on Redux as it working on React. As all I see is that when I toggle the checkboxes all of them reach the same state.
You have !checkbox.id === action.payload.id as your condition logic. As all of your checkbox IDs are 'truthy', then this !checkbox.id evaluates to false, and is the same as writing if(false === action.payload.id).
I suspect you meant to write: if(checkbox.id !== action.payload.id).
What you want to do is pass the id of the checkbox you want to toggle in an action. That's all you need in an action to toggle state. Then in the reducer you want to map over the current state and just return the checkbox for any that don't match the id passed in the action. When the id does match, return a new option spreading the current checkbox's properties into the new object and setting the checked property to it's opposite.
Given this action:
const TOGGLE_CHECKBOX = 'TOGGLE_CHECKBOX'
function toggleCheckbox(id) {
return {
type: TOGGLE_CHECKBOX,
id
}
}
Actions - Redux - Guide to actions and action creators provided by the author of Redux.
This reducer will do the job.
function checkboxReducer(state = [], action = {}) {
switch(action.type) {
case TOGGLE_CHECKBOX:
return state.map(checkbox => {
if (checkbox.id !== action.id) {
return checkbox;
}
return {
...checkbox,
checked: checkbox.isChecked ? false : true,
}
})
default:
return state;
}
}
Reducers - Redux - Guide to reducers and how to handle actions provided by the author of Redux.
Here is a working Code Sandbox to demonstrate it working. You can click the checkboxes to see them toggling as expected.

Print only clicked radio button value(React)

Print selected radio button value in console.
If all radiogroup is answered then print in console only the selected= true radioValue. for example: if NO radiobutton= true its value is 2. It should print value 2. Like that all true radiovalue should print in console.
Thanks
//array of cards coming from the backend
const data = [
{
cardName: 'Do you want sugar in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
},
{
cardName: 'Do you want milk in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
},
{
cardName: 'Do you want low-fat-milk in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
}
];
class CardsList extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: [],
};
}
componentDidMount() {
setTimeout(() => {
// mimic an async server call
this.setState({ cards: data });
}, 1000);
}
onInputChange = ({ target }) => {
const { cards } = this.state;
const nexState = cards.map(card => {
if (card.cardName !== target.name) return card;
return {
...card,
options: card.options.map(opt => {
const checked = opt.radioName === target.value;
return {
...opt,
selected: checked
}
})
}
});
this.setState({ cards: nexState })
}
onSubmit = () => {
console.log(this.state.cards.map(({ cardName, options }) => {
const option = options.filter(({ selected }) => selected)[0]
return ` ${option.radioValue}`
}))
};
onReset = () => {
this.setState({cards:[]});
}
render() {
const { cards } = this.state;
return (
<div>
{
cards.length < 1 ? "Loading..." :
<div>
{cards.map((card, idx) => (
<ul>
{card.cardName}
{card.options.radioName}
{
card.options.map((lo, idx) => {
return <input
key={idx}
type="radio"
name={card.cardName}
value={lo.radioName}
checked={!!lo.selected}
onChange={this.onInputChange}
/>
})
}
</ul>
))
}
< button onClick={this.onSubmit}>Submit</button>
< button onClick={this.onReset}>Clear</button>
</div>
}
</div>
);
}
}
ReactDOM.render(<CardsList />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Change your log in onSubmit to this
console.log(this.state.cards.map(({ cardName, options }) => {
const option = options.filter(({ selected }) => selected)[0]
return `${cardName}: ${option.radioName}`
}))
This way you filter the options to the one, where selected is truthy, and take the first one.
To address your first question, just map over your this.state.cards array before doing the log and check, if there is exactly 1 option, where selected is true. If this is not the case, tell the user in whatever way you want.
Also you can remove your constructor and change it to that:
state = {
cards: [],
}
Because you do not access your props in your constructor
You can go with the answer of #george,
for you to check if either of the radio buttons is clicked for each card, you can run a validation check
let unselectedCards = this.state.cards.filter((card) => {
return !card.options[0].selected && !card.options[1].selected
});
use the unselectedCards variable to highlight the cards.
you can use map options again inside the cards map if you would be having more options.

Testing functions in jest

I need some advice on testing functions in terms of how and what
say I have some state.
state = {
categories: [this is full of objects],
products: [this is also full of objects]
}
then I have this function:
filterProducts = () => {
return this.state.products.filter((product => (
product.categories.some((cat) => (
cat.title == this.state.chosenCategory
))
)))
}
this function filters the products array by working out if the products are part of the selected category.
how would you test this?
I've tried this
let productsStub = [
{id: 1, title: 'wine01', showDescription: false},
{id: 2, title: 'wine02', showDescription: false},
{id: 3, title: 'wine03', showDescription: false}
]
wrapper = shallow(<Menu
categories={categoriesStub}
products={productsStub}
/>);
it('should filter products when searched for', () => {
const input = wrapper.find('input');
input.simulate('change', {
target: { value: '01' }
});
expect(productsStub.length).toEqual(1);
});
what this test (I think) is saying, when I search for 01, I expect the product state (well the stub of the state) to filter and return only 1 result. however the test fails and says expected: 1 received: 3 i.e. the filtering isn't working.
I know I could also do wrapper.instance.filterProducts() but again, I'm not very comfortable on function testing in jest.
any advice? would be great to chat it through with someone
thanks
I replicated your problem statement, but not sure how are you maintaining the state model (props/state). But this might help. :)
Checkout the working example here: https://codesandbox.io/s/6zw0krx15k
import React from "react";
export default class Hello extends React.Component {
state = {
categories: [{ id: 1 }],
products: this.props.products,
selectedCat: 1,
filteredProducts: []
};
filterProducts = value => {
let filteredVal = this.props.products.filter(
product => product.id === parseInt(value)
);
this.setState({
filteredProducts: filteredVal
});
};
setValue = e => {
this.setState({
selectedCat: e.target.value
});
this.filterProducts(e.target.value);
};
render() {
return (
<div>
Filter
<input value={this.state.selectedCat} onChange={this.setValue} />
</div>
);
}
}
import { shallow } from "enzyme";
import Filter from "./Filter";
import React from "react";
let productsStub = [
{ id: 1, title: "wine01", showDescription: false },
{ id: 2, title: "wine02", showDescription: false },
{ id: 3, title: "wine03", showDescription: false }
];
let wrapper = shallow(<Filter products={productsStub} />);
it("should filter products when searched for", () => {
const input = wrapper.find("input");
input.simulate("change", {
target: { value: "1" }
});
expect(wrapper.state().filteredProducts.length).toEqual(1);
});

Categories

Resources