Почему хук useState вызывается дважды?

git: https://github.com/techybolek/DUP-TESTCASE

Этот пример задуман как тестовый пример, демонстрирующий дублирующийся хук для следующего сценария:

  1. Пользователь вводит запрос и нажимает кнопку «Отправить».
  2. Новый объект разговора инициализируется и добавляется в список существующих разговоров.
  3. Вызывается функция ignoreServerAPI с запросом в качестве параметра и функцией обратного вызова, которая будет вызываться для каждого шага ответа. Для каждого запроса может быть несколько шагов ответа, и я хочу отображать их сразу же, как только они поступают с сервера.
  4. Ответ будет добавлен в текущий разговор. Однако ловушка setConversations вызывается дважды для каждого шага, хотя updateCurrentConversation вызывается только один раз.

Вывод консоли:

page.tsx:25 Text response received: Response for query:sample query 1
page.tsx:31 Invoke updateCurrentConversation with response:  Response for query:sample query 1
page.tsx:31 Invoke updateCurrentConversation with response:  Response for query:sample query 1

Источник:

"use client"

import React, { useState, useRef } from 'react';

export default function MyApp() {
  const [input, setInput] = useState('');
  const [conversations, setConversations] = useState([]);
  const messagesEndRef = useRef(null);

  const handleSubmit = async (event) => {
    event.preventDefault();

    const startTime = new Date()
    const newConversation = { query: input, startTime, responses: [] }
    setConversations((prevConversations) => [...prevConversations, newConversation]);

    try {
      await invokeServerAPI(input, textHandler)
    }
    finally {
      setInput('');
    }

    function textHandler(text) {
      console.info("Text response received:", text)
      updateCurrentConversation(text)
    }

    function updateCurrentConversation(data) {
      setConversations((prevConversations) => {
        console.info('Invoke updateCurrentConversation with response: ', data)
        const index = prevConversations.findIndex(conversation => conversation.startTime === startTime);
        const _newConversation = { ...prevConversations[index] };
        _newConversation.responses.push({ data })
        return [
          ...prevConversations.slice(0, index),
          _newConversation,
          ...prevConversations.slice(index + 1)
        ];
      });
    }
  };

  return (
    <div className = "flex flex-col h-screen">
      <div className = "overflow-y-scroll flex-grow p-2">
        {conversations.map((conversation, index) => (
          <div key = {index} className = "mb-5">
            <p>{conversation.startTime.toLocaleString()} <span className = "font-bold">{conversation.query})</span>:</p>
            {conversation.responses
              .map((response, respIndex) => {
                return (
                  <div key = {index * 1000 + respIndex} className = "mb-2">
                      <pre className = "mt-2 bg-gray-100 p-2 rounded border border-gray-300 whitespace-pre-wrap">{response.data}</pre>
                  </div>
                );
              })
            }
          </div>
        ))}
        <div ref = {messagesEndRef} />
      </div>
      <div className = "p-2 bg-gray-800 text-white">
        <form onSubmit = {handleSubmit}>
          <div className = "flex justify-start space-x-4">
            <div className = "flex flex-col" style = {{ marginRight: '1rem', marginLeft: '0' }}>
              <label className = "text-black" style = {{ color: 'black', display: 'block', width: 'auto' }}>Query:</label>
              <input
                type = "text"
                value = {input}
                onChange = {(e) => setInput(e.target.value)}
                className = "p-2 bg-gray-300 text-black rounded-md"
                style = {{ fontSize: '18px', color: 'black', height: '40px', width: '500px' }}
                placeholder = "Enter your query here"
              />
            </div>
            <div className = "flex flex-col items-start">
              <label style = {{ color: 'black', display: 'block', width: 'auto' }}>&nbsp;</label>
              <button type = "submit" className = "bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
                style = {{ paddingLeft: '1rem', paddingRight: '1rem', height: '40px', marginLeft: '0' }}>Send</button>
            </div>
          </div>
        </form>
      </div>
    </div >
  );
}

export function invokeServerAPI(theQuery, theHandler) {

  return new Promise((resolve, reject) => {

    //simulate just one response
    theHandler('Response for query:' + theQuery)
    resolve(null);
  })
}

Есть ли шанс, что вы используете реакцию StrictMode?

zerkms 07.06.2024 02:10

Когда вы вызываете setState, он запускает повторный рендеринг, если значение было изменено.

fordat 07.06.2024 02:32

@zerkms Я не использую строгий режим.

Ya. 07.06.2024 14:42

Ну что ж. оказалось, что строгий режим включен неявно, потому что это приложение nextjs. Отключите его, и теперь setState вызывается только один раз.

Ya. 07.06.2024 18:30

Зачем вам отключать инструменты разработки, которые помогают писать правильный код? Оставьте его включенным и попытайтесь понять, что он включен по умолчанию по какой-то причине (т. е. чтобы помочь вам писать лучший код) и включен только в непроизводственных сборках.

Drew Reese 09.06.2024 08:57

@DrewReese Вероятно, ты прав. Однако я в растерянности, что еще делать.

Ya. 09.06.2024 21:30

Что вы имеете в виду, кроме отключения StrictMode, которое я не рекомендую, единственный вариант — оставить его включенным и воспользоваться преимуществами. Как я уже сказал, если оставить эту опцию включенной, вы сможете писать более качественный код. На мой взгляд, это проще простого. Когда срабатывает дымовая сигнализация, вы не устраняете ее, вынимая батарею, чтобы выключить сигнализацию, а находите источник дыма и тушите его.

Drew Reese 10.06.2024 00:36

@DrewReese Если я позволю ему вызываться дважды, это сломает мое приложение, потому что ответы добавляются дважды. Я пробовал фильтровать, подсчитывать звонки и многое другое, но мне не удалось заставить это работать. Возможно, я что-то упускаю. Я смиренно признаю, что не понимаю причину двойного вызова setState и не знаю, как с этим справиться. Под обработкой я имею в виду, в моем конкретном случае, как предотвратить двойное добавление элементов. Если есть какое-либо руководство, как обрабатывать двойной вызов setState, я был бы признателен.

Ya. 10.06.2024 00:58
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
8
100
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

Если в файле index.js есть React.StrictMode, удалите его.

Я не использую строгий режим.

Ya. 07.06.2024 14:47

ваша игровая площадка stackblitz не работает, не могли бы вы это исправить?

Dileepa Mabulage 07.06.2024 17:41

Извините, пришлось удалить это из поста.

Ya. 07.06.2024 18:41
Ответ принят как подходящий

setState вызывается дважды в строгом режиме в React. В Next JS, чтобы отключить строгий режим, отредактируйте файл next.config.mjs, как показано ниже, и строгий режим будет отключен.

Вы также можете ознакомиться с документацией actStrictMode.

/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: false,
};

export default nextConfig;

Другие вопросы по теме