Search code examples
reactjsasynchronousreduxes6-promise

React/Redux: dispatching and updating the state issues/clarifications


I am using Material UI tables to populate a table with some datas taken from my Redux store and then using a function to remove data.

The table I am using is here https://material-ui.com/demos/tables/ and is the one labelled "Sort and selecting". I will just include the part that interest us and omit all the rest.

So when the component mounts, I get the data from the database by dispatching "startGetClients" and I pass the data in the state of the "ClientsListTable" to populate the table. This works fine.

When clicking on the delete "div" (this is actually an Icon in another component but for testing purpose I have used a simple "div") I take an array with the selected ids and in a "forEach" I dispatch "removeClients" passing the object. If I check the state on Redux dev tool I can see that is correctly updated and the object is removed but I can't update the state of ClientsListTable unless I click again despite the fact the I run the setState() function passing the new data. I know it has something to do with ASYNC but I can't figure this out.

Client has been removed but the state of "ClientsListTable" is not updated despite the fact that I am running setState() immediately after

class ClientsListTable extends Component {

  constructor(props) {
    super(props);

    this.state = {
      order: 'asc',
      orderBy: 'name',
      selected: [],
      data: [],
      page: 0,
      rowsPerPage: 5,
    };
  }

  componentWillMount() {
    this.props.startGetClients().then(() => {
      let data = this.props.clients;
      this.setState( () =>({ data }) );
    });
  }

  handleSelectAllClick = (event, checked) => {
    if (checked) {
      this.setState(state => ({ selected: state.data.map(n => n.id) }));
      return;
    }
    this.setState({ selected: [] });
  };

  handleDeletedData = (selectedIds, data) => {
    selectedIds = this.state.selected;
    data = this.props.clients;

    selectedIds.forEach(id => {
      this.props.removeClients({id: id});
    });

    this.setState( () =>({ data }) );
  }


  render() {
    const { classes } = this.props;
    const { data, order, orderBy, selected, rowsPerPage, page } = this.state;
    const emptyRows = rowsPerPage - Math.min(rowsPerPage, data.length - page * rowsPerPage);

    return (
      <Paper className={classes.root}>

        <div onClick={this.handleDeletedData}>REMOVE ITEMS</div>

        <ClientsListTableToolbar numSelected={selected.length} selectedId={selected} />
        <div className={classes.tableWrapper}>
          <Table className={classes.table} aria-labelledby="tableTitle">
            <ClientsListTableHead
              numSelected={selected.length}
              order={order}
              orderBy={orderBy}
              onSelectAllClick={this.handleSelectAllClick}
              onRequestSort={this.handleRequestSort}
              rowCount={data.length}
            />
            <TableBody>
              {data
                .sort(getSorting(order, orderBy))
                .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
                .map(n => {
                  const isSelected = this.isSelected(n.id);
                  return (
                    <TableRow
                      hover
                      onClick={event => this.handleClick(event, n.id)}
                      role="checkbox"
                      aria-checked={isSelected}
                      tabIndex={-1}
                      key={n.id}
                      selected={isSelected}
                    >
                      <TableCell padding="checkbox">
                        <Checkbox checked={isSelected} />
                      </TableCell>
                      <TableCell component="th" scope="row" padding="none">
                        {n.clientName}
                      </TableCell>
                      <TableCell>{n.lastLogin ? n.lastLogin : 'Never logged in'}</TableCell>
                    </TableRow>
                  );
                })}
              {emptyRows > 0 && (
                <TableRow style={{ height: 49 * emptyRows }}>
                  <TableCell colSpan={6} />
                </TableRow>
              )}
            </TableBody>
          </Table>
        </div>
        <TablePagination
          component="div"
          count={data.length}
          rowsPerPage={rowsPerPage}
          page={page}
          backIconButtonProps={{
            'aria-label': 'Previous Page',
          }}
          nextIconButtonProps={{
            'aria-label': 'Next Page',
          }}
          onChangePage={this.handleChangePage}
          onChangeRowsPerPage={this.handleChangeRowsPerPage}
        />
      </Paper>
    );
  }

}

const mapStateToProps = (state) => ({
  clients: state.clients,
});

const mapDispatchToProps = (dispatch) => ({
  startGetClients: () => dispatch(startGetClients()),
  removeClients: (data) => dispatch(removeClients(data))
});

export default compose(
  withStyles(styles),
  connect(mapStateToProps,mapDispatchToProps),
)(ClientsListTable);

// ACTIONS

export const removeClients = ({ id } = {}) => ({
  type: 'REMOVE_CLIENTS',
  id
});

// REDUCERS

case 'REMOVE_CLIENTS':
  return state.filter( ({id}) => id !== action.id);

Solution

  • It is hard to know without a bit more information about your project but I think the following method is possibly to blame:

    handleDeletedData = (selectedIds, data) => {
        selectedIds = this.state.selected;
        data = this.props.clients;
    
        selectedIds.forEach(id => {
          this.props.removeClients({id: id});
        });
    
        this.setState( () =>({ data }) );
      }
    

    What you're doing here is dispatching a remove event for each ID (asynchronously) and then when you're done you explicitly set the state = data. The problem here is "what is the data at this point in time?" I suspect the first time you call this it sets it using the "data" and when you call it again it sets it using the "new data" from the redux store.

    If you change this so that it depends on the store: this.props.clients rather than the state: this.state.data it should updated when the reducer updates the store.

    Try this, instead of:

    const { classes } = this.props;
    const { data, order, orderBy, selected, rowsPerPage, page } = this.state;
    

    try

    const { classes } = this.props;
    const { order, orderBy, selected, rowsPerPage, page } = this.state;
    const data = this.props.clients
    

    The just remove the "setState" call in your handleDeletedData method. When redux updates the store your properties should change and force the table to render.

    handleDeletedData = (selectedIds, data) => {
        selectedIds = this.state.selected;
        data = this.props.clients;
    
        selectedIds.forEach(id => {
          this.props.removeClients({id: id});
        });
    
    //    this.setState( () =>({ data }) );
      }