Map stops iterating after first iteration in React

Viewed 604

My component is supposed to retrieve the data for courses when the component mounts. The problem that I have is that whether I use the course Id or the course title as the key, I get the following error:

index.js:1 Warning: Each child in a list should have a unique "key" prop.

I have looked through the react docs, here on Stack Overflow, and tried different ways to get it to work. The only way I can get it to partially work is by adding an index as a parameter for map. When I use this method, I run into another problem and that is, it stops after the first iteration, even though there are 10 items. How can I fix this?

Here is my code:

CoursesPage.js

import React from 'react';
import { connect } from 'react-redux';
import * as courseActions from '../../redux/actions/courseActions';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';

class CoursesPage extends React.Component {

    componentDidMount() {
        this.props.actions.loadCourses().catch(error => {
            alert("Loading courses failed" + error);
        });

    }


    render() {
        return (
            <>
                <h2>Courses</h2>

                

{this.props.courses.map((course, index) => (

                    <div key={course[index].title}>{course[index].title}</div>


                ))}
            </>
        );
    }
}

CoursesPage.propTypes = {
    courses: PropTypes.array.isRequired,
    actions: PropTypes.object.isRequired
}

function mapStateToProps(state) {
    return {
        courses: state.courses
    }

}

function mapDispatchToProps(dispatch) {

    return {
        actions: bindActionCreators(courseActions, dispatch)
    }

}

export default connect(mapStateToProps, mapDispatchToProps)(CoursesPage);

My mock data:

const courses = [
    {
        id: 1,
        title: "Securing React Apps with Auth0",
        slug: "react-auth0-authentication-security",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 2,
        title: "React: The Big Picture",
        slug: "react-big-picture",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 3,
        title: "Creating Reusable React Components",
        slug: "react-creating-reusable-components",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 4,
        title: "Building a JavaScript Development Environment",
        slug: "javascript-development-environment",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 5,
        title: "Building Applications with React and Redux",
        slug: "react-redux-react-router-es6",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 6,
        title: "Building Applications in React and Flux",
        slug: "react-flux-building-applications",
        authorId: 1,
        category: "JavaScript"
    },
    {
        id: 7,
        title: "Clean Code: Writing Code for Humans",
        slug: "writing-clean-code-humans",
        authorId: 1,
        category: "Software Practices"
    },
    {
        id: 8,
        title: "Architecture Applications for the Real World",
        slug: "architecting-applications-dotnet",
        authorId: 1,
        category: "Software Architecture"
    },
    {
        id: 9,
        title: "Becoming an Outlier: Reprogramming the Developer Mind",
        slug: "career-reboot-for-developer-mind",
        authorId: 1,
        category: "Career"
    },
    {
        id: 10,
        title: "Web Component Fundamentals",
        slug: "web-components-shadow-dom",
        authorId: 1,
        category: "HTML5"
    }
];

const authors = [
    { id: 1, name: "Cory House" },
    { id: 2, name: "Scott Allen" },
    { id: 3, name: "Dan Wahlin" }
];

const newCourse = {
    id: null,
    title: "",
    authorId: null,
    category: ""
};

module.exports = {
    newCourse,
    courses,
    authors
};

Edit: I am using Redux Thunk.

Here is my actionType.js file:

export const CREATE_COURSE = "CREATE_COURSE";
export const LOAD_COURSES_SUCCESS = "LOAD_COURSES_SUCCESS";

Here is my CourseActions.js file:

import * as types from './actionTypes';
import * as courseApi from "../../api/courseApi";

export function createCourse(course) {
    return { type: types.CREATE_COURSE, course };
}

export function loadCourseSuccess(courses) {
    return { type: types.LOAD_COURSES_SUCCESS, courses };
}

export function loadCourses() {
    return function (dispatch) {
        return courseApi.getCourses().then(courses => {
            dispatch(loadCourseSuccess(courses));
        }).catch(error => {
            throw error;
        })

    }
}

Here is my courseReducer.js file:

import * as types from '../actions/actionTypes';

export default function courseReducer(state = [], action) {

    switch (action.type) {
        case types.CREATE_COURSE:
            return [...state, { ...action.course }];

        case types.LOAD_COURSES_SUCCESS:
            return [...state, { ...action.courses }];

        default:
            return state;
    }
}

Any help would be appreciated.

Thanks.

P.S. I know that you should use Id for key. But the way it has to be done for now is using the title of the course as the key.

5 Answers

With your edits, I think we can more effectively help you. For future, it would be beneficial to post an example of your code not working on https://codesandbox.io/

Also to help yourself out when you debug, isolate the react component from the use of redux. This will allow you to ensure your react component renders when given data, then focus on getting redux to provide you the data your react component has.

You can do this by first defining mockdata inside your react component, then moving that mock data to your reducer, then finally replacing the mock data with the live api call.

On to the code:

You have two issues: the first is that you want to index into the array courses but instead due to a typo, you are actually using the property accessor into the object course

key={course[index].title}

As your question states you must use the title as the key simply change the div to be:

<div key={course.title}>{course.title}</div> and your code should work as expected.

Once you have addressed that, then re-enable loading data from your API call using Redux, and you can address the issues with Redux.

Looking at your reducer, you have a clear bug, and depending on your use case, a potential bug:

case types.LOAD_COURSES_SUCCESS:
            return [...state, { ...action.courses }];

action.courses is an array, and the code is creating a new array that contains all the elements of the previous array state and adding a new object, which contains the contents of a destructured array.courses array.

Which that does is effectively append a single object to your array, and the object is comprised of elements from your courses array. The item index becomes the key, and the item itself is the value.

You can visualize it here: https://codesandbox.io/s/divine-pond-nr3j8

Instead you want

return [...state, ...action.courses];

The second, potential bug, is that you are actually appending the results of the courses api. Which for subsequent calls to load courses, you will duplicate the data. That may be what you want, but I am going to assume that is not what you want.

So instead, your LOAD_COURSES_SUCCESS case should be rewritten to simply be:

return [...action.courses];

The issue is here:

{this.props.courses.map((course, index) => (

                    <div key={course[index].title}>{course[index].title}</div>


                ))}
//course in itself is data

Solution:

{this.props.courses.map((course, index) => (

                    <div key={`${course.title}-${index}`}>{course.title}</div>


                ))}

Better way would always to take care unique id for key

Since each course has a unique ID field, the problem could be solved by using the id as a key

render() {
  return (
    <>
      <h2>Courses</h2>



      {this.props.courses.map(course => (

        <div key={course.id}>{course.title}</div>


      ))}
    </>
  );
}

There are many ways you can pass the key when it is iterating...

var courses = this.state.courses.map(function(course, index) {
        return(
            <div key={index}>
                <div key={course.title} id={course.title}>
                    <h2 key={"header"+course.title}>{course.title}</h2>
                </div>
            </div>
        )
    });

But you should try to pass a unique id rather than a string.

Here's the code snippet for render method. Give it a try.

*Update = use course title as key

render() {
    const courseList = this.props.courses.map(course => (
        <div key={course.title}></div>
    ));
        
    return (
        <>
            <h2>Courses</h2>
            <div>{courseList}</div>
        </>
    );
}
                
Related