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.

DanV
fonte
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.

Felix Kling
fonte
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.

Tikkes
fonte
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.

Clareza
fonte
"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