Concurrent Mode in React: adaptando aplicativos da web para dispositivos e velocidade da internet

Neste artigo, apresentarei o Modo simultâneo no React. Vamos descobrir o que é: quais são as funcionalidades, que novas ferramentas surgiram e como otimizar o funcionamento das aplicações web com a ajuda delas para que tudo voe para os usuários. O modo simultâneo é um novo recurso do React. Sua tarefa é adaptar o aplicativo a diferentes dispositivos e velocidades de rede. Até o momento, o Modo Simultâneo é um experimento que pode ser alterado pelos desenvolvedores da biblioteca, o que significa que não há novas ferramentas no estábulo. Eu te avisei e agora vamos.



Atualmente, existem duas limitações para os componentes de renderização: potência do processador e taxa de transferência de dados da rede. Sempre que algo precisa ser mostrado ao usuário, a versão atual do React tenta renderizar cada componente do início ao fim. Não importa se a interface pode congelar por alguns segundos. É a mesma história com a transferência de dados. O React irá esperar por absolutamente todos os dados de que o componente precisa, em vez de desenhá-los peça por peça.







O regime competitivo resolve esses problemas. Com ele, o React pode pausar, priorizar e até mesmo desfazer operações que estavam bloqueadas anteriormente, portanto, no modo simultâneo, você pode começar a renderizar componentes independentemente de todos os dados terem sido recebidos ou apenas parte deles.



O modo simultâneo é arquitetura de fibra



O modo competitivo não é uma coisa nova que os desenvolvedores decidiram adicionar de repente, e tudo funcionou bem ali. Preparado para seu lançamento com antecedência. Na versão 16, o motor React foi mudado para uma arquitetura Fibre, que, a princípio, se assemelha ao agendador de tarefas do sistema operacional. O planejador distribui recursos computacionais entre os processos. Ele pode mudar a qualquer momento, então o usuário tem a ilusão de que os processos estão rodando em paralelo.



A arquitetura de fibra faz a mesma coisa, mas com componentes. Apesar de já estar no React, a arquitetura Fiber parece estar em animação suspensa e não usa suas capacidades ao máximo. O Modo Competitivo o ligará com força total.



Quando você atualiza um componente no modo normal, deve desenhar um novo quadro na tela. Até que a atualização seja concluída, o usuário não verá nada. Nesse caso, o React funciona de forma síncrona. A fibra usa um conceito diferente. A cada 16 ms há uma interrupção e uma verificação: a árvore virtual mudou, surgiram novos dados? Nesse caso, o usuário os verá imediatamente.



Por que 16ms? Os desenvolvedores do React buscam redesenhar a tela a uma taxa próxima a 60 quadros por segundo. Para ajustar 60 atualizações em 1000 ms, você precisa fazer aproximadamente a cada 16 ms. Daí a figura. O Modo Competitivo sai da caixa e adiciona novas ferramentas que tornam a vida do front-end melhor. Vou lhe contar sobre cada um em detalhes.



Suspense



O suspense foi introduzido no React 16.6 como um mecanismo para carregar componentes dinamicamente. No modo simultâneo, essa lógica é preservada, mas aparecem oportunidades adicionais. Suspense torna-se um mecanismo que funciona em conjunto com a biblioteca de carregamento de dados. Solicitamos um recurso especial por meio da biblioteca e lemos os dados dele.



O Suspense lê simultaneamente dados que ainda não estão prontos. Como? Solicitamos os dados e, até que cheguem na íntegra, já começamos a lê-los em pequenos pedaços. O mais legal para os desenvolvedores é gerenciar a ordem em que os dados carregados são exibidos. Suspense permite que você exiba os componentes da página simultaneamente e independentemente um do outro. Isso torna o código simples: basta olhar para a estrutura do Suspense para ver em que ordem os dados são solicitados.



Uma solução típica para carregar páginas no React "antigo" é Fetch-On-Render. Nesse caso, solicitamos os dados após a renderização dentro de useEffect ou componentDidMount. Esta é a lógica padrão quando não há Redux ou outra camada de dados. Por exemplo, queremos desenhar 2 componentes, cada um dos quais precisa de dados:



  • Solicitação de componente 1
  • Expectativa…
  • Obter dados -> renderizar componente 1
  • Solicitação de componente 2
  • Expectativa…
  • Obter dados -> renderizar componente 2


Nessa abordagem, o próximo componente é solicitado somente depois que o primeiro é renderizado. É longo e inconveniente.



Vamos considerar outra maneira, Fetch-Then-Render: primeiro solicitamos todos os dados, depois desenhamos a página.



  • Solicitação de componente 1
  • Solicitação de componente 2
  • Expectativa…
  • Obtendo o componente 1
  • Obtendo o componente 2
  • Renderização de componentes


Nesse caso, movemos o estado da solicitação em algum lugar para cima - delegamos à biblioteca para trabalhar com os dados. O método funciona muito bem, mas há uma nuance. Se um dos componentes demorar muito mais para carregar do que o outro, o usuário não verá nada, embora já pudéssemos mostrar algo a ele. Vejamos um exemplo de código da demonstração com 2 componentes: Usuário e Postagens. Envolvemos os componentes em Suspense:



const resource = fetchData() // -    React
function Page({ resource }) {
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </Suspense>
    )
}


Pode parecer que essa abordagem é próxima ao Fetch-On-Render, quando solicitamos os dados após renderizar o primeiro componente. Mas, na verdade, usar o Suspense obterá os dados muito mais rápido. Isso se deve ao fato de que ambas as solicitações são enviadas em paralelo.



No Suspense, você pode especificar o fallback, o componente que queremos exibir, e passar o recurso implementado pela biblioteca de recuperação de dados dentro do componente. Nós o usamos como está. Dentro dos componentes, solicitamos dados do recurso e chamamos o método read. Esta é uma promessa que a biblioteca nos faz. O Suspense entenderá se os dados foram carregados e, em caso afirmativo, mostrará.



Observe que os componentes estão tentando ler dados que ainda estão em processo de recebimento:



function User() {
    const user = resource.user.read()
    return <h1>{user.name}</h1>
}
function Posts() {
    const posts = resource.posts.read()
    return //  
}


Nas demos atuais de Dan Abramov, tal coisa é usada como um esboço para um recurso .



read() {
    if (status === 'pending') {
        throw suspender
    } else if (status === 'error') {
        throw result
    } else if (status === 'success') {
        return result
    }
}




Se o recurso ainda estiver carregando, lançamos o objeto Promise como uma exceção. Suspense captura essa exceção, percebe que é uma promessa e continua carregando. Se, em vez de uma promessa, chegar uma exceção com qualquer outro objeto, ficará claro que o pedido terminou com erro. Quando o resultado final for retornado, o Suspense irá exibi-lo. É importante para nós obter um recurso e chamar um método nele. Como é implementado internamente é uma decisão dos desenvolvedores da biblioteca, o principal é que o Suspense entenda sua implementação.



Quando solicitar dados? Perguntar no topo da árvore não é uma boa ideia, porque talvez nunca seja necessário. A melhor opção é fazer isso imediatamente ao navegar dentro dos manipuladores de eventos. Por exemplo, obtenha o estado inicial por meio de um gancho e, a seguir, faça uma solicitação de recursos assim que o usuário clicar no botão.



É assim que ficará no código:



function App() {
    const [resource, setResource] = useState(initialResource)
    return (
        <>
            <Button text='' onClick={() => {
                setResource(fetchData())
            }}>
            <Page resource={resource} />
        </>
    );
}


O suspense é incrivelmente flexível. Ele pode ser usado para exibir componentes um após o outro.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </Suspense>
)


Ou, ao mesmo tempo, ambos os componentes precisam ser envolvidos em um Suspense.



return (
    <Suspense fallback={<h1>Loading user and posts...</h1>}>
        <User />
        <Posts />
    </Suspense>
)


Ou carregue os componentes separadamente, envolvendo-os em um Suspense independente. O recurso será carregado por meio da biblioteca. É muito legal e conveniente.



return (
    <>
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User />
        </Suspense>
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </>
)


Além disso, os componentes do Limite de erro detectarão erros dentro do Suspense. Se algo deu errado, podemos mostrar que o usuário carregou, mas as postagens não, e dar um erro.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User resource={resource} />
        <ErrorBoundary fallback={<h2>Could not fetch posts</h2>}>
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </ErrorBoundary>
    </Suspense>
)


Agora, vamos dar uma olhada em outras ferramentas que podem desbloquear todos os benefícios do regime competitivo.



SuspenseList



O SuspenseList simultaneamente ajuda a controlar a ordem de carregamento do Suspense. Se precisássemos carregar vários Suspense estritamente um após o outro sem ele, eles teriam que ser aninhados um dentro do outro:



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
            <Suspense fallback={<h1>Loading facts...</h1>}>
                <Facts />
            </Suspense>
        </Suspense>
    </Suspense>
)


SuspenseList torna isso muito mais fácil:



return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
        <Suspense fallback={<h1>Loading facts...</h1>}>
            <Facts />
        </Suspense>
    </Suspense>
)


A flexibilidade do SuspenseList é incrível. Você pode aninhar SuspenseList como desejar e personalizar a ordem de carregamento interna, pois será conveniente para exibir widgets e quaisquer outros componentes.



useTransition



Um gancho especial que adia a atualização do componente até que esteja totalmente pronto e remove o estado de carregamento intermediário. Para que serve? React se esforça para fazer a transição o mais rápido possível ao mudar de estado. Mas às vezes é importante levar o seu tempo. Se uma parte dos dados é carregada em uma ação do usuário, geralmente, no momento do carregamento, mostramos um carregador ou esqueleto. Se os dados chegarem muito rapidamente, o carregador não terá tempo para completar nem mesmo meia volta. Ele piscará e depois desaparecerá e desenharemos o componente atualizado. Nesses casos, é mais sensato não mostrar o carregador.



É aqui que entra useTransition. Como funciona no código? Chamamos o gancho useTransition e especificamos o tempo limite em milissegundos. Se os dados não vierem dentro do tempo especificado, ainda mostraremos o carregador. Mas se os conseguirmos mais rápido, haverá uma transição instantânea.



function App() {
    const [resource, setResource] = useState(initialResource)
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })
    return <>
        <Button text='' disabled={isPending} onClick={() => {
            startTransition(() => {
                setResource(fetchData())
            })
        }}>
        <Page resource={resource} />
    </>
}


Às vezes, não queremos mostrar o carregador quando vamos para a página, mas ainda precisamos mudar algo na interface. Por exemplo, durante a transição, bloqueie o botão. Então, a propriedade isPending será útil - informará que estamos no estágio de transição. Para o usuário, a atualização será instantânea, mas é importante notar aqui que a magia useTransition afeta apenas os componentes envolvidos no Suspense. O próprio UseTransition não funcionará.



As transições são comuns em interfaces. A lógica responsável pela transição seria ótima para costurar no botão e integrar na biblioteca. Caso exista um componente responsável pela transição entre as páginas, pode-se envolver o onClick em handleClick, que passa pelos adereços para o botão, e mostra o estado isDisabled.



function Button({ text, onClick }) {
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })

    function handleClick() {
        startTransition(() => {
            onClick()
        })
    }

    return <button onClick={handleClick} disabled={isPending}>text</button>
}


useDeferredValue



Portanto, existe um componente com o qual fazemos as transições. Às vezes, surge a seguinte situação: o usuário deseja ir para outra página, recebemos alguns dados e estamos prontos para mostrá-los. Ao mesmo tempo, as páginas diferem ligeiramente umas das outras. Nesse caso, seria lógico mostrar ao usuário os dados desatualizados até que todo o resto seja carregado.



Agora o React não sabe como: na versão atual, apenas os dados do estado atual podem ser exibidos na tela do usuário. Mas useDeferredValue no modo simultâneo pode retornar uma versão adiada de um valor, mostrar dados desatualizados em vez de um carregador piscando ou fallback no momento da inicialização. Este gancho assume o valor para o qual queremos obter a versão adiada e o atraso em milissegundos.



A interface se torna superfluida. As atualizações podem ser feitas com uma quantidade mínima de dados e todo o resto é carregado gradualmente. O usuário tem a impressão de que o aplicativo é rápido e suave. Em ação, useDeferredValue tem a seguinte aparência:



function Page({ resource }) {
    const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 })
    const isDeferred = resource !== deferredResource;
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={deferredResource} isDeferred={isDeferred}/>
            </Suspense>
        </Suspense>
    )
}


Você pode comparar o valor dos adereços com o obtido por meio de useDeferredValue. Se forem diferentes, a página ainda está carregando.



Curiosamente, useDeferredValue permitirá que você repita o truque do carregamento lento não apenas para dados que são transmitidos pela rede, mas também para remover o congelamento da interface devido a grandes cálculos.



Por que isso é ótimo? Dispositivos diferentes funcionam de maneira diferente. Se você executar um aplicativo usando useDeferredValue em um novo iPhone, a transição de uma página para outra será instantânea, mesmo se as páginas forem pesadas. Mas ao usar o depurado, o atraso aparecerá até mesmo em um dispositivo poderoso. UseDeferredValue e o modo concorrente se adaptam ao hardware: se funcionar lentamente, a entrada ainda voará e a própria página será atualizada conforme o dispositivo permitir.



Como faço para mudar um projeto para o modo simultâneo?



O Modo competitivo é um modo, então você precisa habilitá-lo. Como uma chave seletora que faz o Fiber funcionar em plena capacidade. Por onde você começa?



Removemos o legado. Nós nos livramos de todos os métodos obsoletos no código e nos certificamos de que eles não estão nas bibliotecas. Se o aplicativo funcionar bem no React.StrictMode, então está tudo bem - a mudança será fácil. A complicação potencial são os problemas nas bibliotecas. Nesse caso, você precisa atualizar para uma nova versão ou alterar a biblioteca. Ou abandone o regime competitivo. Depois de se livrar do legado, tudo o que resta é mudar de root.



Com a chegada do Modo Simultâneo, três modos de conexão raiz estarão disponíveis:



  • O modo de

    ReactDOM.render(<App />, rootNode)

    renderização antigo será descontinuado após o lançamento do modo competitivo.
  • Modo de bloqueio

    ReactDOM.createBlockingRoot(rootNode).render(<App />)

    Como etapa intermediária, será adicionado o modo de bloqueio, que dá acesso a algumas das oportunidades de modo competitivo em empreendimentos onde haja legados ou outras dificuldades de realocação.
  • Modo competitivo

    ReactDOM.createRoot(rootNode).render(<App />)

    Se tudo estiver bem, não houver legado e o projeto puder ser mudado imediatamente, substitua o render no projeto por createRoot - e para um futuro brilhante.


conclusões



As operações de bloqueio dentro do React tornam-se assíncronas ao alternar para fibra. Estão surgindo novas ferramentas que tornam mais fácil adaptar o aplicativo aos recursos do dispositivo e à velocidade da rede:



  • Suspense, graças ao qual você pode especificar a ordem de carregamento dos dados.
  • SuspenseList, com o qual é ainda mais conveniente.
  • useTransition para criar transições suaves entre componentes envolvidos em Suspense.
  • useDeferredValue - para mostrar dados desatualizados durante I / O e atualizações de componentes


Experimente experimentar o modo simultâneo enquanto ele ainda está desativado. O modo simultâneo permite que você alcance resultados impressionantes: carregamento rápido e suave de componentes em qualquer ordem conveniente, interface superfluida. Os detalhes são descritos na documentação, também há demonstrações com exemplos que você deve estudar por conta própria. E se você está curioso sobre como funciona a arquitetura de fibra, aqui está um link para uma palestra interessante.



Avalie seus projetos - o que pode ser melhorado com as novas ferramentas? E quando o regime competitivo acabar, fique à vontade para mudar. Tudo vai ficar ótimo!



All Articles