Back home

Divisão refinada de componentes e questões de propriedade estatal

Depois de dividir um estado em múltiplas verdades locais, a sequência se torna um evento probabilístico

Os sintomas desse bug online são muito semelhantes aos “ocasionais”, mas não são aleatórios.

Na mesma página, um estado de negócio existirá em diferentes formas em diferentes componentes: parâmetros de URL, estado do componente pai, estado local do componente filho, cache retornado pela solicitação e até mesmo valores derivados calculados por um determinado seletor. Quanto mais finos os componentes são decompostos, mais se tornam essas “verdades parciais”. Enquanto “quem pode escrever, quem sabe e quem é responsável pelo tempo” não convergir primeiro para uma regra, a causa raiz do status de erro online mudará de “um determinado trecho de código está escrito incorretamente” para “vários trechos de código foram escritos corretamente, mas a ordem de escrita é instável”.

A coisa mais difícil nesse tipo de problema é solucionar problemas, não reparar. Porque parece que cada componente é muito razoável: eles mantêm seu próprio pequeno estado, armazenamento em cache e carregamento. Mas quando combinados, o sistema não possui um proprietário de estado único, e o desempenho final é: basta atualizar, alternar as guias e tentar novamente. Se você quiser usar logs para conectar links, descobrirá que o mesmo campo vem de props, cache local e solicitação de pacotes de retorno. Quem cobre quem depende inteiramente do ritmo de renderização e do atraso da solicitação.

O julgamento deste artigo é simples:

Se a divisão do componente não convergir primeiro para “quem escreve o estado, quem conhece os detalhes e quem é responsável pelo tempo”, o mesmo estado será cortado em múltiplas verdades parciais e a sequência de atualização se tornará um evento probabilístico. Finalmente, os benefícios da reutilização serão substituídos por estados de erro ocasionais, e ocorrerá o custo de renderização repetida e solução de problemas.

Abaixo, usarei uma solução de problemas de status de erro on-line muito típica para explicar como convergir passo a passo.

Cena: Um estado de erro “ocasional”

A página é uma lista + barra de filtro superior.

  • Há consulta: ?tab=all&sort=latest&city=sh na URL
  • A barra de filtro superior é dividida em vários pequenos componentes: Guia, Classificação, Cidade suspensa
  • O componente de lista armazena em cache o “resultado da última solicitação” para evitar oscilações ao trocar os filtros.

O feedback do usuário é: Ao alternar rapidamente entre Guia e Classificação, às vezes a lista exibirá “itens de filtro da nova classificação”, mas os dados da lista ainda serão “os resultados da classificação antiga”. Clicar na mesma classificação novamente funciona bem.

À primeira vista, parece que a interface é inconsistente, mas a captura do pacote mostra que a interface não retorna nenhum problema e o eco sort no corpo retornado também está correto. Em outras palavras, o servidor está certo e o que está errado é o “estado exibido” no front-end.

Primeiro erro de julgamento: pensei que fosse uma condição de corrida de solicitação

A intuição fará você suspeitar: a solicitação de A é lenta, a solicitação de B é rápida, B volta primeiro e renderiza o resultado correto e então A volta e sobrescreve o resultado antigo.

Este tipo de condição de corrida é realmente comum, então primeiro adicionamos o requestId e descartamos o pacote de retorno: apenas a última solicitação enviada é aceita.

Depois de ficar online, o problema diminuiu um pouco, mas não desapareceu. Explique que a “cobertura do pacote de retorno” não é o único canal.

O valor desta etapa é: ela primeiro corta um pedaço de um espaço problemático aparentemente grande. Agora é certo que pelo menos alguns dos estados defeituosos não são causados ​​pela ordem da rede.

O segundo erro de julgamento: pensei que fosse um erro na lógica do cache.

Então vamos dar uma olhada no cache do componente de lista.

A estratégia de cache é:

  • Passe filters dos adereços
  • O componente de lista usa useRef internamente para salvar lastGoodData
  • Acione a solicitação se filters for alterado
  • Continue exibindo lastGoodData durante a solicitação e substitua-o quando novos dados retornarem

Não há nada de errado com essa lógica de “redução de cintilação”, mas ela enterra uma premissa: filters deve ser uma fonte de verdade estável e única. Caso contrário, é fácil aparecer: filters mudou, mas a lista ainda usa o antigo lastGoodData e, superficialmente, pensar-se-á que é apenas um encobrimento durante o carregamento.

Achei que era porque a referência do objeto filters estava instável, fazendo com que o tempo de acionamento do efeito fosse confuso. Alterado para chave de serialização explícita: filtersKey = tab + sort + city.

Ainda não há cura.

A verdadeira causa raiz: a propriedade estatal está destruída

Depois de finalmente digitar todos os logs, o problema ficou claro:

  • O componente Tab se preocupa apenas com tab, ele irá:
  • Ao clicar, setLocalTab(nextTab) será destacado imediatamente
  • Então onChange(nextTab) notifica o componente pai
  • Vá para setFilters({ ... }) após receber o componente pai
  • O componente de classificação também tem o mesmo padrão
  • Para oferecer suporte à “atualização retomável”, o componente pai irá:
  • Primeiro extraia os filtros iniciais da análise de URL
  • Então escreva para o estado
  • Os componentes da lista também recebem:
  • Adereços filters do componente pai
  • e um getCachedResult(filtersKey) do módulo de cache

Em outras palavras, um estado tem pelo menos três conjuntos de fontes:

  1. Estado local do subcomponente: usado para interação de feedback imediato
  2. O estado do componente pai: como filtros no nível da página
  3. Módulo de cache: usado como back-end de exibição de dados

Não existe um contrato estrito de “ordem de gravação” entre eles.

O link onde ocorre o problema geralmente é o seguinte:

  • Classificação de pontos do usuário
  • O componente Classificar atualiza imediatamente seu estado local e a IU exibe “Nova classificação selecionada”
  • O filters do componente pai não teve tempo de ser atualizado (ou foi atualizado, mas não será repassado até o próximo quadro)
  • O componente da lista recalculará filtersKey neste momento
  • Mas não conta os novos filtros do componente pai
  • Em vez disso, um caminho derivado combina o valor local de Sort (como por meio de contexto ou seletor)
  • filtersKey alterado, então a lista foi para o módulo de cache e buscou um resultado antigo que “parecia correspondente”
  • Quando a solicitação retornar, por causa da política de descarte de requestId, desde que não seja a última vez, ela será descartada
  • A UI final tem uma combinação estranha: a barra de filtro é o novo valor e os dados da lista vêm do cache antigo

Isso significa que “vários trechos de código foram escritos corretamente, mas a ordem é instável”.

A divisão de componentes corta os direitos de gravação do estado em pedaços: o componente filho grava uma cópia primeiro para feedback instantâneo da interação, o componente pai grava outra cópia para reprodução e o cache grava outra cópia para fins de experiência. Não há nada de errado com nada, mas o sistema carece de uma propriedade estatal unificada.

Como parar de falar: primeiro decida a propriedade e depois fale sobre reutilização

A solução para este tipo de problema é escrever os direitos de escrita do estado, as regras de derivação e as estratégias de encobrimento num contrato executável.

Acabei decidindo por três regras.

Regra 1: Os filtros de página têm apenas uma fonte gravável

Os componentes filhos não mantêm mais seu próprio estado de filtros locais.

O feedback interativo imediato é fornecido pelo estado do componente pai, e o componente filho é responsável apenas por enviar eventos e não por armazenar valores. Isto é:

  • Submontagem: onSelect(next)
  • Componente pai: setFilters(reduce(prev, action))
  • Exibição de subcomponentes: somente leitura value={filters.sort}

O custo disso é: o subcomponente se tornará “burro” e mais adereços precisarão ser repassados quando reutilizados. Mas o que ela troca é um certo caminho de escrita.

Regra 2: Valores derivados devem indicar explicitamente a origem

Todas as chaves usadas para solicitações e armazenamento em cache só podem ser geradas a partir de filters.

É proibido gerar outro conjunto de chaves diretamente do contexto, seletor ou URL.

Isto pode parecer uma mística, mas é para evitar “campos com o mesmo nome vindos de fontes diferentes”. Assim que sort puder vir do estado local e do estado pai, você encontrará “um conjunto de UI e outro conjunto de dados” de hoje.

Regra 3: O cache apenas exibe os detalhes e não participa do julgamento do status.

O módulo de cache fornece apenas um recurso: getLastGoodData(filtersKey).

Ele não pode determinar qual é o filterKey atual, muito menos usar os dados de uma chave antiga como o “resultado atual”.

As etapas específicas são:

  • O status da solicitação de lista é claro: currentFiltersKey é passado do componente pai
  • Permitido ao mostrar: -data = loading ? cache[currentFiltersKey] ?? null : result
  • mas o cache nunca afeta os filtros de volta

Isso reduz o cache de “participação no status do sistema” para “exibição puramente dos resultados financeiros”. Irá sacrificar um pouco de experiência, por exemplo, alguns interruptores ficarão vazios por um tempo, mas retornará a certeza.

Contra-exemplo: “Promover todos os estados para componente pai” também falhará

Depois de ouvir isto, algumas pessoas dirão: Basta elevar todos os estados ao nível mais alto.

A versão que vi falhar é: a camada superior torna-se a única fonte da verdade, mas também contém:

  • Sincronização de URL
  • Persistência local
  • Limitação de solicitações
  • acerto no cache
  • Estado interativo da UI (passar o mouse, focar, painel aberto)

Como resultado, o redutor de nível superior se torna o núcleo do negócio, e qualquer pequena interação precisa passar por uma série de lógica, e o custo da modificação dispara. No final, todos começaram a contorná-lo e adicionaram secretamente o estado local de volta aos subcomponentes, e o sistema retornou a “múltiplas verdades locais”.

Portanto, a chave é “clarar quais estados exigem propriedade exclusiva”.

Geralmente julgo isso em uma frase: contanto que afete solicitações, chaves de cache ou consistência entre componentes, ele deve ser de propriedade exclusiva. Transientes de UI puros podem ser localizados.

Limite aplicável: Nem todas as divisões de componentes causarão armadilhas

Esta crítica não se refere à divisão de componentes em si.

A divisão foi originalmente planejada para controlar a complexidade, mas tem um custo implícito: o “limite da escrita do estado” deve ser projetado adicionalmente.

Quando a página satisfizer qualquer uma das seguintes condições, esta armadilha aparecerá facilmente:

  • Possui URL/recuperação persistente
  • Existem solicitações ou cancelamentos simultâneos
  • Há uma exibição em cache
  • Tenha filtros compartilhados entre componentes

Por outro lado, se a página for muito simples, o estado não afetará a solicitação e não houver cache e recuperação, o estado local não se tornará um desastre.

Resumo

Quebrar os componentes em pequenos pedaços não trará automaticamente benefícios de reutilização; primeiro criará problemas de propriedade estatal.

Se você não responder primeiro “Quem pode escrever, quem sabe os detalhes e quem é o responsável pelo tempo”, o sistema responderá por si só: quem renderizar primeiro terá a palavra final, e quem voltar tarde cobrirá.

Quando ocorrem estados de erro “ocasionais” na linha, você descobrirá que o que é realmente caro é recuperar um estado de múltiplas verdades parciais em um determinado link de gravação. \

FAQ

What to read next

Related

Continue reading