In my React state, I want to reorder an array of 3 objects by always putting the selected one in the middle while keeping the others in ascending order.
Right now, I'm using an order property in each object to keep track of the order, but this might not be the best approach.
For example :
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2'
order: 1
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item2, item1, item3]
Now, let's imagine that a user selects item2. I will update the selected state property accordingly, but how can I update the items property to end up with a result like this:
this.state = {
selected: 'item2',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2'
order: 2
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item1, item2, item3]
How would you do it? I have seen some lodash utility functions that could help but I would like to acplish this in vanilla JavaScript.
In my React state, I want to reorder an array of 3 objects by always putting the selected one in the middle while keeping the others in ascending order.
Right now, I'm using an order property in each object to keep track of the order, but this might not be the best approach.
For example :
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2'
order: 1
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item2, item1, item3]
Now, let's imagine that a user selects item2. I will update the selected state property accordingly, but how can I update the items property to end up with a result like this:
this.state = {
selected: 'item2',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2'
order: 2
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item1, item2, item3]
How would you do it? I have seen some lodash utility functions that could help but I would like to acplish this in vanilla JavaScript.
Share Improve this question edited May 31, 2017 at 21:09 Heretic Monkey 12.1k7 gold badges61 silver badges131 bronze badges asked May 31, 2017 at 21:03 Thomas MilanThomas Milan 3091 gold badge5 silver badges14 bronze badges 3- Where would you put the selected item if there are an even number of items? – Heretic Monkey Commented May 31, 2017 at 21:06
- FYI, you should be using this.setState() to set react state. Never directly set the state besides in the constructor. – James Kraus Commented May 31, 2017 at 21:12
- Well, that obviously wouldn't work but that's something to consider. A great solution could work for any odd number of items I guess even though it could get pretty plex. However, in my use case, I only need to handle 3 items so I thought there might be some clever solution to make it work ! Edit @JamesKraus Indeed ! This is just an illustration of the desired result :) – Thomas Milan Commented May 31, 2017 at 21:13
4 Answers
Reset to default 2You could do something crude like this:
// Create a local shallow copy of the state
var items = this.state.items.slice()
// Find the index of the selected item within the current items array.
var selectedItemName = this.state.selected;
function isSelectedItem(element, index, array) {
return element.id === selectedItemName;
};
var selectedIdx = items.findIndex(isSelectedItem);
// Extract that item
var selectedItem = items[selectedIdx];
// Delete the item from the items array
items.splice(selectedIdx, 1);
// Sort the items that are left over
items.sort(function(a, b) {
return a.id < b.id ? -1 : 1;
});
// Insert the selected item back into the array
items.splice(1, 0, selectedItem);
// Set the state to the new array
this.setState({items: items});
This assumes the size of the items array is always 3!
I'm gonna be lazy and just outline the steps you need to take.
- Pop the selected item out of the starting array
- Push the first item of the starting array into a new array
- Push the selected item into the new array
- Push the last item of the starting array into the new array
- Set your state to use the new array
You can do something like:
NOTE: This works assuming there would three items in the array. However, if there are more we just need to specify the index position in the insert function.
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2',
order: 2
},
{
id: 'item3',
order: 3
}
]
};
// To avoid mutation.
const insert = (list, index, newListItem) => [
...list.slice(0, index), // part of array before index arg
newListItem,
...list.slice(index) // part of array after index arg
];
// Get selected item object.
const selectedValue = value => this.state.items.reduce((res, val) => {
if (val.id === selectedValue) {
res = val;
}
return res;
}, {});
const filtered = this.state.items.filter(i => i.id !== state.selected);
const result = insert(filtered, 1, selectedValue(this.state.selected));
We can get rid of the extra reduce
if instead of storing id
against selected you store either the index of the item or the whole object.
Of course we need to use this.setState({ items: result })
. This solution would also ensure we are not mutating the original state array at any point.
I put together a fully working example what can be extended on so you can experiment with different ways to achieve your intended use-case.
In this case I created a button ponent and rendered three of them to provide a means of changing the selected state.
Important things to remember, always use the setState()
function for updating React Class state. Also, always work on state arrays and objects with a cloned variable as you'll want to update the whole object/array at once. Don't modify attributes of pointer variables pointing to state objects or arrays.
It is very possible to add bugs to your code by referencing state objects/arrays and then changing their properties (accidentally or not) by modifying the pointer referencing the object. You will lose all guarantees on how the state will update, and paring prevState
or nextState
with this.state
may not work as intended.
/**
* @desc Sub-ponent that renders a button
* @returns {HTML} Button
*/
class ChangeStateButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = ({
//any needed state here
});
}
handleClick(e) {
//calls parent method with the clicked button element and click state
this.props.click(e.nativeEvent.toElement.id);
}
render() {
return (
<button
id = {this.props.id}
name = {this.props.name}
className = {this.props.className}
onClick = {this.handleClick} >
Reorder to {this.props.id}!
</button>
);
}
}
/**
* @desc Creates button ponents to control items order in state
* @returns {HTML} Bound buttons
*/
class ReorderArrayExample extends React.Component {
constructor(props) {
super(props);
this.reorderItems = this.reorderItems.bind(this);
this.state = ({
selected: 'item1',
//added to give option of where selected will insert
selectedIndexChoice: 1,
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2',
order: 1
},
{
id: 'item3',
order: 3
}
]
});
}
reorderItems(selected) {
const {items, selectedIndexChoice} = this.state,
selectedObjectIndex = items.findIndex(el => el.id === selected);
let orderedItems = items.filter(el => el.id !== selected);
//You could make a faster reorder algo. This shows a working method.
orderedItems.sort((a,b) => { return a.order - b.order })
.splice(selectedIndexChoice, 0, items[selectedObjectIndex]);
//always update state with setState function.
this.setState({ selected, items: orderedItems });
//logging results to show that this is working
console.log('selected: ', selected);
console.log('Ordered Items: ', JSON.stringify(orderedItems));
}
render() {
//buttons added to show functionality
return (
<div>
<ChangeStateButton
id='item1'
name='state-button-1'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item2'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item3'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
</div>
);
}
}
/**
* @desc React Class renders full page. Would have more ponents in a real app.
* @returns {HTML} full app
*/
class App extends React.Component {
render() {
return (
<div className='pg'>
<ReorderArrayExample />
</div>
);
}
}
/**
* Render App to DOM
*/
/**
* @desc ReactDOM renders app to HTML root node
* @returns {DOM} full page
*/
ReactDOM.render(
<App/>, document.getElementById('root')
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root">
<!-- This div's content will be managed by React. -->
</div>