React unique “key” error

2019-05-06 19:37发布

问题:

I'm going through a tutorial for todo list in React and ran into the following error, I have spent quite a time and just can't find the mistake..here is the error and the code for the component and this is the code for the course repo(problem appears on this commit):

https://github.com/andrewjmead/react-course-todo-app/commit/0521f151705f78cb9f8d69262eb093f1431cb9ca

Any help much appreciated.

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of TodoList. See fb.me/react-warning-keys for more information.

Also there is an error in terminal, for the spread operator in case TOGGLE_TODO

return {
...todo, // here
completed: nextCompleted,
completedAt: nextCompleted ? moment().unix() : undefined
};

var React = require('react');
var { connect } = require('react-redux');
import Todo from 'Todo';
var TodoAPI = require('TodoAPI');

export var TodoList = React.createClass ({
    render: function() {
        var { todos, showCompleted, searchText } = this.props;
        var renderTodos = () => {
            if(todos.length === 0) {
                return (
                    <p className="container__message">No tasks</p>
                );
            }
            return TodoAPI.filterTodos(todos, showCompleted, searchText).map((todo) => {
                return (
                    //add unique key prop to keep track of individual components
                    <Todo key={todo.id} {...todo} />
                );
            });
        };
        return (
            <div>
                {renderTodos()}
            </div>
        );  
    }
});

export default connect(
    (state) => {
        return state;
    }
)(TodoList);

Reducers:

var uuid = require('uuid');
var moment = require('moment');

export var searchTextReducer = (state = '', action) => {
    switch (action.type) {
        case 'SET_SEARCH_TEXT':
            return action.searchText;
        default: 
            return state;
    };
};

export var showCompletedReducer = (state = false, action) => {
    switch (action.type) {
        case 'TOGGLE_SHOW_COMPLETED':
            return !state;
        default: 
            return state;
    };    
};

export var todosReducer = (state = [], action) => {
    switch(action.type) {
        case 'ADD_TODO':
            return [
                ...state,
                {
                    text: action.text,
                    id: uuid(),
                    completed: false,
                    createdAt: moment().unix(),
                    completedAt: undefined                   
                }
            ];
        case 'TOGGLE_TODO':
            return state.map((todo) => {
                if(todo.id === action.id) {
                    var nextCompleted = !todo.completed;

                    return {
                        ...todo,
                        completed: nextCompleted,
                        completedAt: nextCompleted ? moment().unix() : undefined
                    };
                } else {
                    return todo;
                }
            });
        case 'ADD_TODOS':
            return [
                ...state,
                ...action.todos
            ];
        default: 
            return state;
    }
};


Webpack:

var webpack = require('webpack');

module.exports = {
  entry: [
    'script!jquery/dist/jquery.min.js',
    'script!foundation-sites/dist/js/foundation.min.js',
    './app/app.jsx'
  ],
  externals: {
      jquery: 'jQuery'
  },
  plugins: [
      new webpack.ProvidePlugin({
          '$': 'jquery',
          'jQuery': 'jquery'
      })
  ],
  output: {
    path: __dirname,
    filename: './public/bundle.js'
  },
  resolve: {
    root: __dirname,
    modulesDirectories: [
        'node_modules',
        './app/components',
        './app/api'
    ],
    alias: {
        applicationStyles: 'app/styles/app.scss',
        actions: 'app/actions/actions.jsx',
        reducers: 'app/reducers/reducers.jsx',
        configureStore: 'app/store/configureStore.jsx'
    },
    extensions: ['', '.js', '.jsx']
  },
  module: {
    loaders: [
      {
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015']
        },
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/
      }
    ]
  },
  devtool: 'cheap-module-eval-source-map'
};

回答1:

node-uuid is deprecated, check here: https://www.npmjs.com/package/uuid

You can update your package.json by installing uuid and see if it helps:

npm install uuid

Just don't forget to update var uuid = require('node-uuid'); to var uuid = require('uuid'); in your other files.

P.S. Do you get any errors in your terminal when you run a webpack?



回答2:

In React, when you are rendering multiple equal components (in your case, the todos) you need to add a unique key to each one of them, that's because React needs to know how they are going to be treated in the virtual dom.

You can do various things to fix this:

  1. In the for loop, create an index variable and increment it by 1 each time a loop finishes, then set that as the key for each rendered component.

  2. If you're fetching your todos from an api, set an id to each todo and use it as your component key.

  3. Use a random number generator to set a unique key on each of your todos.

The best approaches are the #2 and #3, I see that in your case you're trying to do the #2 (setting the key by todo id) but I think it's undefined, check it.

Another solution is to use a uuid on each rendered component/todo.

To do that, you can install node-uuid.

Run: npm i --save node-uuid

Then do the import in your file: import uuid from 'node-uuid' or const uuid = require('node-uuid')

Now change your code to be like this:

return TodoAPI.filterTodos(todos, showCompleted, searchText).map((todo) => {
  return (
    //add unique key prop to keep track of individual components
    <Todo key={uuid()} {...todo} />
  );
});

And then you're good to go.



回答3:

I bet todo.id is undefined and therefore not unique. Can you include todos in your example?



回答4:

You can add index param to your map function, then pass that index to your Todo component:

export var TodoList = React.createClass ({
    render: function() {
        var { todos, showCompleted, searchText } = this.props;
        var renderTodos = () => {
            if(todos.length === 0) {
                return (
                    <p className="container__message">No tasks</p>
                );
            }
            return TodoAPI.filterTodos(todos, showCompleted, searchText).map((todo, index) => {    // here <====
                return (
                    //add unique key prop to keep track of individual components
                    <Todo key={`key-${index}`} {...todo} />
                );
            });
        };
        return (
            <div>
                {renderTodos()}
            </div>
        );  
    }
});