0

It seems like I have come across a very common problem in React/Redux. There's a multitude of people having this exact issue, still the cause can vary by a lot so I couldn't identify a possible solution. I'm trying to build a webpage with a list that can be re-ordered by pressing the up and down buttons.

I have managed to track down the updated state (array) until the reducer but the component is not updating. I believe I'm not correctly updating the component's state.

Here's my component:

import React, { Component } from 'react';
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { changeOrder } from "../actions/index.js";

// let list = ["apple", "banana", "orange", "mango", "passion fruit"];

export class Home extends Component {
  constructor(props){
    super(props);
    this.renderList = this.renderList.bind(this);
  }

  goUp(position){
    this.props.changeOrder(position, "up");
  }

  goDown(position){
    this.props.changeOrder(position, "down");
  }

  renderList(fruit){
    const position = this.props.list.indexOf(fruit);
    return (
          <div key={fruit}>
            <li>{fruit}</li>
            <button onClick={() => this.goUp(position)}>Up</button>
            <button onClick={() => this.goDown(position)}>Down</button>
          </div>
        );
  }

  render() {
      return (
        <div>
          <h1>This is the home component</h1>
          <ol>
            {this.props.list.map(this.renderList)}
          </ol>
        </div>
      );
  }
}

function mapStateToProps(state){
  console.log(state);
    return { list: state.list };
}

function mapDispachToProps(dispatch) {
    return bindActionCreators({ changeOrder }, dispatch);
}

export default connect(mapStateToProps, mapDispachToProps)(Home);

Action:

export const GO_UP = "GO_UP";
export const GO_DOWN = "GO_DOWN";

export function changeOrder(position, direction) {
    console.log("position: " + position + " | direction: " + direction);
   switch (direction) {
       case "up": {
           return {
                type: GO_UP,
                payload: position
           };
       }
       case "down": {
           return {
               type: GO_DOWN,
               payload: position
           };
       }
   }
}

Reducer:

import { GO_UP, GO_DOWN } from "../actions/index.js";
let list = ["apple", "banana", "orange", "mango", "passion fruit"];

function arrayMove(arr, old_index, new_index) {
    if (new_index >= arr.length) {
        var k = new_index - arr.length + 1;
        while (k--) {
            arr.push(undefined);
        }
    }
    arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
    return arr; // for testing
}

export default function (state = list, action){
    // debugger;
    switch (action.type){
        case GO_UP: {
            let newState = arrayMove(state, action.payload, action.payload -1);
            console.log(newState);
            return newState;
        }
        case GO_DOWN: {
            let newState = arrayMove(state, action.payload, action.payload +1);
            console.log(newState);
            return newState;
        }
        default: return list;
    }
}

Here's the reducer's Index.js:

import { combineReducers } from 'redux';
import ReducerList from "./reducer_list";

const rootReducer = combineReducers({
  list: ReducerList
});

console.log(rootReducer);

export default rootReducer;

And lastly index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import ReduxPromise from "redux-promise";

import App from './components/app';
import reducers from './reducers';

const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}>
    <App />
  </Provider>
  , document.querySelector('.container'));

Any help would be greatly appreciated!

Scaccoman
  • 455
  • 7
  • 16
  • You are mutating state (your `arrayMove`), react-redux `connect` (specifically `maStateToProps`) requires references to change for it to update the component. see this previous answer https://stackoverflow.com/questions/48475537/reactjs-reduxmapstatetoprops-not-rendering-the-component-on-state-change/48476215#48476215 – dashton Feb 05 '18 at 12:44

2 Answers2

3

When you are using react-redux you want to make sure you do not mutate state, rather you need to create a new state object (a shallow copy). Refer here for more information. Redux will not know if the state has updated if you mutate the previous state.

in arrayMove you are mutating the current state. instead of

arrayMove(state, action.payload, action.payload -1);

use the following,

arrayMove(state.slice(), action.payload, action.payload -1);
Skyler
  • 656
  • 5
  • 14
  • This solution worked. I'm however not sure that this is the best approach to it. I should probably update the arrayMove function to return a new array, correct? – Scaccoman Feb 05 '18 at 13:31
  • Well you can do the new array creation inside arrayMove as well. – Skyler Feb 05 '18 at 13:35
  • a note for others: if you use `lodash.merge` the way to update state is, in the reducer, `return {...merge(state, action.payload}`(if you are the type to organise the structure of your payload for simple reducers). This is different to using `const newState = merge(state, action.payload); return newState` as it won't update. – mewc Feb 24 '20 at 21:48
1

The problem is located in your reducer in the function arrayMove(). You're changing existing array named arr. After the change of it, React component doesn't know that property state.list actually changed, because the memory reference is following the same old array (with changed content).

To tell your application, that list is actually changed, you need to return NEW array. Like after all your logic in arrayMove, at the end you need to return something like:

return [].concat(arr);

In the functional programming terminology, your arrayMove is a dirty function with side effects, because it's mutating existing object/array by it's reference. The "clean" function should return a new instance of an object.

steppefox
  • 1,784
  • 2
  • 14
  • 19