elemetrics
elemetrics

Reputation: 340

React re renders all items in array when new items are added despite using unique keys for all items

Original Post

I have a page that renders each item in an array contained in a redux state using Array.prototype.map. The array is initially populated when the componentWillMount() callback is called and is then appended when the user requests more data, i.e, when the user scrolls to the bottom of the page and if there are more items to be fetched. I make sure that the redux state is not mutated when the array is appened by using Array.prototype.concat(). However, when more items are added to the array, it seems like react re renders every item in the array, and not just the new items despite setting a unique key for each item. My source code for rendering the components is as follows:

this.props.state.list.map((item, index) => {
  return (
    <Grid className={classes.column} key={item._id} item xs={6} sm={6} md={4}>
      <Card className={classes.card}>
        <CardActionArea className={classes.cardAction}>
          <div className={classes.cardWrapper}>
            <div className={classes.outer}>
              <div className={classes.bgWrapper}>
                <img
                  className={[classes.bg, "lazyload"].join(" ")}
                  data-sizes="auto"
                  data-src={genSrc(item.pictures[0].file)}
                  data-srcset={genSrcSet(item.pictures[0].file)}
                  alt={item.name}
                />
              </div>
            </div>
            <CardContent className={classes.cardContent}>
              <div className={classes.textWrapper}>
                <div className={classes.textContainer}>
                  <Typography
                    gutterBottom
                    className={[classes.text, classes.title].join(" ")}
                    variant="title"
                    color="inherit"
                  >
                    {item.name}
                  </Typography>
                  <Typography
                    className={[classes.text, classes.price].join(" ")}
                    variant="subheading"
                    color="inherit"
                  >
                    {item.minPrice === item.maxPrice
                      ? `$${item.minPrice.toString()}`
                      : `$${item.minPrice
                          .toString()
                          .concat(" - $", item.maxPrice.toString())}`}
                  </Typography>
                </div>
              </div>
            </CardContent>
          </div>
        </CardActionArea>
      </Card>
    </Grid>
  );
});

Here's my reducer if you think it could have something do with that:

import {
  FETCH_PRODUCTS_PENDING,
  FETCH_PRODUCTS_SUCCESS,
  FETCH_PRODUCTS_FAILURE
} from "../actions/products";

let defaultState = {
  list: [],
  cursor: null,
  calls: 0,
  status: null,
  errors: []
};

const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case FETCH_PRODUCTS_PENDING:
      return {
        ...state,
        status: "PENDING"
      };
    case FETCH_PRODUCTS_SUCCESS:
      return {
        ...state,
        calls: state.calls + 1,
        cursor: action.payload.data.cursor ? action.payload.data.cursor : null,
        list: action.payload.data.results
          ? state.list.concat(action.payload.data.results)
          : state.list,
        status: "READY"
      };
    case FETCH_PRODUCTS_FAILURE:
      return {
        ...state,
        status: "FAIL",
        call: state.calls + 1,
        errors: [...state.errors, action.payload]
      };
    default:
      return state;
  }
};

export default reducer;

I read from answers to questions similar to mine that setting a shouldComponentUpdate() method for every component that is generated from the array might do the trick, but there are no clear examples as to how this can be done. I would really appreciate the help. Thanks!

Edit 1

I should also specify that the components rendered from the array is completely unmounted and mounted again in the browser DOM when new items are added to the array. Ideally I would want the browser to never unmount existing components and only append newly rendered components.

Edit 2

Here's the entire code for the component that renders the list.

class Products extends React.Component {

  componentDidMount() {
    const { fetchNext, fetchStart, filter } = this.props
    fetchStart(filter)

    window.onscroll = () => {
      const { errors, cursor, status } = this.props.state
      if (errors.length > 0 || status === 'PENDING' || cursor === null) return
      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
        fetchNext(filter, cursor)
      }
    }
  }

  render() {
    const { classes, title } = this.props

    return (
      <div className={classes.container}>
        <Grid container spacing={0}>
          {
            this.props.state.status && this.props.state.status === 'READY' && this.props.state.list &&
            this.props.state.list.map((item, index) => {
              return (
                <Grid className={classes.column} key={item._id} item xs={6} sm={6} md={4}>
                  <Card className={classes.card}>
                    <CardActionArea className={classes.cardAction}>
                      <div className={classes.cardWrapper}>
                        <div className={classes.outer}>
                          <div className={classes.bgWrapper}>
                            <img className={[classes.bg, 'lazyload'].join(' ')} data-sizes='auto' data-src={genSrc(item.pictures[0].file)} data-srcset={genSrcSet(item.pictures[0].file)} alt={item.name} />
                          </div>
                        </div>
                        <CardContent className={classes.cardContent}>
                          <div className={classes.textWrapper}>
                            <div className={classes.textContainer}>
                              <Typography gutterBottom className={[classes.text, classes.title].join(' ')} variant="title" color='inherit'>
                                {item.name}
                              </Typography>
                              <Typography className={[classes.text, classes.price].join(' ')} variant='subheading' color='inherit'>
                                {item.minPrice === item.maxPrice ? `$${item.minPrice.toString()}` : `$${item.minPrice.toString().concat(' - $', item.maxPrice.toString())}`}
                              </Typography>
                            </div>
                          </div>
                        </CardContent>
                      </div>
                    </CardActionArea>
                  </Card>
                </Grid>
              )
            })
          }
        </Grid>
      </div>
    )
  }
}

Upvotes: 2

Views: 2756

Answers (1)

elemetrics
elemetrics

Reputation: 340

So it turns out it was a conditional render that was causing the rendered list to disappear. In my case I set it so that the component would only render the list if all ongoing fetch operations had completed, so of course the list would disappear between fetch calls.

Changing the following from

    {
      this.props.state.status && this.props.state.status === 'READY' && this.props.state.list &&
        this.props.state.list.map((item, index) => {
          return <Item />
        }
    }

to this, thereby removing the check for the completed fetch state solved my problem.

    {
      this.props.state.list &&
        this.props.state.list.map((item, index) => {
          return <Item />
        }
    }

Upvotes: 2

Related Questions