useEffect - Impede loop infinito ao atualizar o estado


9

Eu gostaria que o usuário pudesse classificar uma lista de itens de tarefas. Quando os usuários selecionam um item em uma lista suspensa, ele define o sortKeyque criará uma nova versão do setSortedTodose acionará a useEffectchamada e setSortedTodos.

O exemplo abaixo funciona exatamente como eu quero, no entanto, o eslint está me pedindo para adicionar todosà useEffectmatriz de dependência e, se o fizer, causa um loop infinito (como seria de esperar).

const [todos, setTodos] = useState([]);
const [sortKey, setSortKey] = useState('title');

const setSortedTodos = useCallback((data) => {
  const cloned = data.slice(0);

  const sorted = cloned.sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });

  setTodos(sorted);
}, [sortKey]);

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

Exemplo ao vivo:

Eu estou pensando que tem que haver uma maneira melhor de fazer isso que mantém o sorriso feliz.


11
Apenas uma observação: O sortretorno de chamada pode ser justo: o return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());que também tem a vantagem de comparar o código de idioma se o ambiente tiver informações razoáveis ​​de código de idioma. Se você preferir, também pode desestruturar: pastebin.com/7X4M1XTH
— TJ Crowder

Que erro está eslintlançando?
— Luze

Você poderia atualizar a pergunta para fornecer um exemplo reproduzível mínimo executável do problema usando Snippets de pilha (o [<>]botão da barra de ferramentas)? Os snippets de pilha suportam React, incluindo JSX; aqui está como fazer um . Dessa forma, as pessoas podem verificar se as suas soluções propostas não têm o problema de loop infinito ...
— TJ Crowder

Essa é uma abordagem realmente interessante e um problema realmente interessante. Como você diz, você pode entender por que o ESLint acha que precisa adicionar todosà matriz de dependência useEffecte ver por que não deveria. :-)
— TJ Crowder

Eu adicionei o exemplo ao vivo para você. Realmente quero ver isso respondido.
— TJ Crowder

Respostas:


8

Eu diria que isso significa que não é o ideal. A função é realmente dependente todos. Se setTodosfor chamada em outro lugar, a função de retorno de chamada deverá ser recalculada, caso contrário, ela opera com dados antigos.

Por que você armazena a matriz classificada no estado de qualquer maneira? Você pode usar useMemopara classificar os valores quando a chave ou a matriz são alteradas:

const sortedTodos = useMemo(() => {
  return Array.from(todos).sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });
}, [sortKey, todos]);

Depois faça referência a sortedTodostodos os lugares.

Exemplo ao vivo:

Não há necessidade de armazenar os valores classificados no estado, pois você sempre pode derivar / calcular a matriz classificada da matriz "base" e da chave de classificação. Eu diria que também torna seu código mais fácil de entender, pois é menos complexo.


Oh bom uso de useMemo. Apenas uma pergunta secundária, por que não usar .localCompareno tipo?
— Tikkes

2
Isso seria realmente melhor. Eu apenas copiei o código do OP e não prestei atenção nisso (não é realmente relevante para o problema).
— Felix Kling

Solução realmente simples e fácil de entender.
— TJ Crowder

Ah sim, useMemo! Esqueci disso :)
— DanV

3

O motivo do loop infinito é porque todos não correspondem à referência anterior e o efeito será executado novamente.

Por que usar um efeito para uma ação de clique? Você pode executá-lo em uma função como esta:

const [todos, setTodos] = useState([]);

function sortTodos(e) {
    const sortKey = e.target.value;
    const clonedTodos = [...todos];
    const sorted = clonedTodos.sort((a, b) => {
        return a[sortKey.toLowerCase()].localeCompare(b[sortKey.toLowerCase()]);
    });

    setTodos(sorted);
}

e no seu menu suspenso, faça um onChange.

    <select onChange="sortTodos"> ......

A propósito, sobre a dependência, o ESLint está certo! Seus Todos, no caso descrito acima, são uma dependência e devem estar na lista. A abordagem na seleção de um item está errada e, portanto, é seu problema.


2
"sort retornará uma nova instância de uma matriz" Não é o método de classificação interno. Classifica a matriz no local. data.slice(0)cria a cópia.
— Felix Kling

Isso não é verdade quando você faz um, setStatepois ele não edita o objeto existente e, portanto, o clona internamente. Palavras erradas na minha resposta, é verdade. Eu vou editar isso.
— Tikkes

setStatenão clona dados. Por que você pensa isso?
— Felix Kling

11
@ Tikkes - Não, setStatenão clona nada. Felix e o OP estão corretos, você precisa copiar a matriz antes de classificá-la.
— TJ Crowder

Ok, desculpe. Eu preciso de mais algumas leituras internas, parece. Você realmente tem que copiar e não alterar o existente state.
— Tikkes

0

O que você precisa fazer aqui é usar a forma funcional de setState:

  const [todos, setTodos] = useState(exampleToDos);
    const [sortKey, setSortKey] = useState('title');

    const setSortedTodos = useCallback((data) => {

      setTodos(currTodos => {
        return currTodos.sort((a, b) => {
          const v1 = a[sortKey].toLowerCase();
          const v2 = b[sortKey].toLowerCase();

          if (v1 < v2) {
            return -1;
          }

          if (v1 > v2) {
            return 1;
          }

          return 0;
        });
      })

    }, [sortKey]);

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

Codesandbox de trabalho

Mesmo se você estiver copiando o estado para não alterar o original, ainda não há garantia de que você obterá o valor mais recente, devido à definição de estado como assíncrono. Além disso, a maioria dos métodos retornará uma cópia superficial, portanto você pode acabar alterando o estado original de qualquer maneira.

O uso do funcional setStategarante que você obtenha o valor mais recente do estado e não mude o valor do estado original.


"e não modifique o valor do estado original." Não acho que o React passe uma cópia para o retorno de chamada. .sortaltera a matriz no local, para que você ainda precise copiá-la.
— Felix Kling

Hmm, bom ponto, na verdade não consigo encontrar nenhuma confirmação de que ele passe na cópia do estado, embora eu me lembre de ter lido algo assim antes.
— Clarity

Encontre-o aqui: reactjs.org/docs/… . 'podemos usar o formulário de atualização funcional de setState. Ele nos permite especificar como o estado precisa mudar sem fazer referência ao estado atual '
— Clarity

Não leio isso como uma cópia. Significa apenas que você não precisa fornecer o estado atual como dependência "externa".
— Felix Kling
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.