Home > Enterprise >  Newly added items to list do not get _id key prop added until hard refresh
Newly added items to list do not get _id key prop added until hard refresh

Time:04-07

I'm trying to make a todo list using Nextjs and mongodb.

I am able to add items to my list, and they are rendered immediately but the unique id sent with the todo from mongodb isn't applied until the page is refreshed, and I get the warning:

next-dev.js?3515:25 Warning: Each child in a list should have a unique "key" prop.

Check the render method of `HomePage`. See https://reactjs.org/link/warning-keys for more information.
    at Todo (webpack-internal:///./components/todo.js:10:17)
    at HomePage (webpack-internal:///./pages/index.js:28:21)
    at MyApp (webpack-internal:///./pages/_app.js:15:24)
    at ErrorBoundary (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:20638)
    at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:23179)
    at Container (webpack-internal:///./node_modules/next/dist/client/index.js:241:5)
    at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:830:24)
    at Root (webpack-internal:///./node_modules/next/dist/client/index.js:983:26)

I suspect it is an async issue but after trying to figure it out myself and online I am hoping someone will shine some light on why this is happening.

Here is my backend code:

import {
  getAllTodos,
  insertTodo,
  connectDatabase,
} from "../../helpers/db-util";

async function handler(req, res) {
  let client;

  try {
    client = await connectDatabase();
  } catch (error) {
    res
      .status(500)
      .json({ message: error.message || "Error connecting to MongoDB." });

    return;
  }

  if (req.method === "GET") {
    try {
      const todosList = await getAllTodos("todos");

      res.status(200).json({ todos: todosList });
    } catch (error) {
      res.status(500).json({
        message: error.message || "Unable to fetch todos from database.",
      });
    }
  }

  if (req.method === "POST") {
    const { text } = req.body;

    if (!text || text.trim() === "") {
      res
        .status(422)
        .json({ message: "You must not have a todo with no text." });
      client.close();
      return;
    }

    const newTodo = { text };

    let result;
    let result2;

    try {
      result = await insertTodo(client, "todos", newTodo);
      result2 = await getAllTodos("todos");
      res.status(201).json({
        message: "Todo successfully added!",
        todo: newTodo,
        todos: result2,
      });
      // console.log(result, result2);
    } catch (error) {
      res.status(500).json({ message: error.message || "Unable to add todo." });
    }
  }
  client.close();
}

export default handler;

and here is the code to the helper functions they utilize:

import { MongoClient } from "mongodb";

const connectionString = `mongodb srv://${process.env.mongodb_username}:${process.env.DB_PASS}@${process.env.mongodb_clustername}.e79y2.mongodb.net/${process.env.mongodb_db_name}?retryWrites=true&w=majority`;

export async function connectDatabase() {
  const client = await MongoClient.connect(connectionString);

  return client;
}

export async function insertTodo(client, collection, todo) {
  const db = client.db();
  const result = await db.collection(collection).insertOne(todo);
  return result;
}

export async function getAllTodos(collection) {
  const client = await connectDatabase();

  const db = client.db();

  const todos = await db.collection(collection).find().toArray();

  return todos;
}

On the front end I initially load the todos with getServerSideProps and all of those have the key of _id properly applied, but when I add a new todo I get the key warning, and even though in a console.log of the newly created item will show the _id as apart of the todo object, it isn't applied to the list of todos that is mapped over.

export default function HomePage(props) {
  const { todos } = props;

  const [todosList, setTodosList] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  function getTodos() {
    const parsedTodos = JSON.parse(todos);
    setTodosList(parsedTodos);
  }

  useEffect(() => {
    getTodos();
  }, [todos]);

  return (
    <div>
      <Head>
        <title>Next Todos</title>
        <meta name="description" content="NextJS todos app" />
      </Head>

      <main>
        <TodoInput
          getTodos={getTodos}
          setTodosList={setTodosList}
          todosList={todosList}
          setIsLoading={setIsLoading}
        />
        {!isLoading &&
          todosList.map((todo) => (
            <Todo key={todo._id} id={todo._id} text={todo.text} />
          ))}
      </main>
    </div>
  );
}

export async function getServerSideProps(context) {
  let client;
  client = await connectDatabase();
  // console.log(client);
  const todosList = await getAllTodos("todos");
  const allTodos = JSON.stringify(todosList);

  return {
    props: {
      todos: allTodos,
    },
  };
}

and here is the Todos input form and submit handler:

const TodoInput = (props) => {
  async function postNewTodo(enteredTodo) {
    await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ text: enteredTodo }),
      headers: { "Content-Type": "application/json" },
    })
      .then((response) => response.json())
      .then((data) => {
        props.setIsLoading(true);
        console.log(data.todos);
        props.setTodosList([
          ...props.todosList,
          { id: data.todo._id, text: data.todo.text },
        ]);
        props.setIsLoading(false);
      });
  }
  const todoInputRef = useRef();
  const handleSubmit = (e) => {
    e.preventDefault();

    const enteredTodo = todoInputRef.current.value;

    postNewTodo(enteredTodo);

    // props.setTodosList([...props.todosList, { text: enteredTodo }]);
  };
  return (
    <Fragment>
      <form onSubmit={handleSubmit}>
        <input type="text" required ref={todoInputRef} />
        <button>Add Todo</button>
      </form>
    </Fragment>
  );
};

export default TodoInput;

I tried to use a loading state to slow down the mapping of the newly added object so that it would have time to properly have its key applied to no avail.

Any help as to why this is happening would be greatly appreciated.

CodePudding user response:

as can be seen in your code,at the time when you add todo you are haveing id as member of todo item but at the time when you render items you are using _id,which is undefined and so all the todo items in array have same value for _id = undefined,so using id instead of _id will clear the warning like this

    <main>
    <TodoInput
      getTodos={getTodos}
      setTodosList={setTodosList}
      todosList={todosList}
      setIsLoading={setIsLoading}
    />
    {!isLoading &&
      todosList.map((todo) => (
        <Todo key={todo.id} id={todo.id} text={todo.text} />
      ))}
  </main>

CodePudding user response:

I do not much about nextjs! Here is a react option!

Since the data your using is coming in an async way try calling useEffect with async too

async function getTodos() {
    const parsedTodos = await JSON.parse(todos);
    setTodosList(parsedTodos);
  }

or if your not going to the key use this

<main>
        <TodoInput
          getTodos={getTodos}
          setTodosList={setTodosList}
          todosList={todosList}
          setIsLoading={setIsLoading}
        />
        {!isLoading &&
          todosList.map((todo, i) => (
            <Todo key={i} id={todo._id} text={todo.text} />
          ))}
      </main>

Where i is the todo index in that array

CodePudding user response:

Solved!

props.setTodosList([
  ...props.todosList,
  { _id: data.todo._id, text: data.todo.text },
]);

in todo-input.js was initially labeled as id instead of _id.

Thank you to the two of you who tried to help me with this.

  • Related