2

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 accomplish this in vanilla JavaScript.

3
  • Where would you put the selected item if there are an even number of items? 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. 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 complex. 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 :) Commented May 31, 2017 at 21:13

4 Answers 4

2

You 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!

Sign up to request clarification or add additional context in comments.

4 Comments

You should not make direct calls to .splice() on the state object. You need to use calls to this.setState
Sorry, I'm not a React guru, so I was answering from the "vanilla javascript" perspective. If you need to do something like setState, add a few steps to my answer above: 1) copy the state to a local variable at the beginning. 2) do the manipulation as suggested. 3) this.setState(newStateStuff)
Updated answer as suggested (from what I understand) by @JamesKraus
Thank you the answer. I've used a similar approach in a previous project using Immutable.js, Gist with example. I was hoping there would be a more clever algo to solve this problem. However, your solution seems great and that's the one I will use in my project !
2

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

3 Comments

Does this assume that the array of items starts out as with 2 items? Seems like OP indicated the items list is always 3 items...
Yeah, I can probably improve my answer
it doesn't work when the starting array has this order [1, 3, 2], and if user select 1, then if we pop 1, then push 3 to the new array and then push 1 and finally 3, it will be [3, 1, 2] which is not correct
0

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.

Comments

0

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 component 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 comparing prevState or nextState with this.state may not work as intended.

/**
  *	@desc Sub-component 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 components 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 components 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.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">
  <!-- This div's content will be managed by React. -->
</div>

2 Comments

Whoever drive-by down-voted my answer without so much as leaving a comment - why? It works. It answers the question and uses the React documentation recommended design pattern. I even added buttons to create an example of the array being reordered. Seriously, it adds no value to down-vote without a comment to further the dialog.
Thank you for the great snippet. This works great and is close to what I wanted. However, and I think it is because I wasn't clear enough in my question, the order property was added only to keep track of the ordering. I thought it would be easier than sorting the array itself. But if you can reorder the array without using this property, I'd love to know the algo that would do the trick !

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.