Fila de renderização em componentes ReactJS. 😱

Em diversos projetos que já trabalhei, sempre houve um momento que múltiplos componentes de Modais (janela que exibe um conteúdo adicional em uma camada acima da página atual) eram exibidos simultaneamente na tela. Geralmente são componentes que tratam de assuntos diferentes e que não possuem conhecimento da existência dos outros componentes, assim dificultando a maneira de tratar as condições.

Isso causa uma má experiência para o usuário que está acessando naquele momento.

Mas não é só fazer algumas funções para controlar?

function Page() {
  const [modal1, setModal1] = useState(false);
  const [modal2, setModal2] = useState(false);

  const openModal1 = () => setModal1(true);

  const closeModal1 = () => {
    setModal1(false);
    openModal2();
  }

  const openModal2 = () => setModal2(true);

  const closeModal2 = () => setModal2(false);

  return (
    <>
      {modal1 && <Modal1 />}
      {modal2 && <Modal2 />}
    </>
  );
}

De fato funciona, mas será que é a melhor alternativa?

E se for necessário adicionar mais um componente?

function Page() {
  const [modal1, setModal1] = useState(false);
  const [modal2, setModal2] = useState(false);
  const [modal3, setModal3] = useState(false);

  const openModal1 = () => setModal1(true);

  const closeModal1 = () => {
    setModal1(false);
    openModal2();
  }

  const openModal2 = () => setModal2(true);

  const closeModal2 = () => {
    setModal2(false);
    openModal3();
  }

  const openModal3 = () => setModal3(true);

  const closeModal3 = () => setModal3(false);

  return (
    <>
      {modal1 && <Modal1 />}
      {modal2 && <Modal2 />}
      {modal3 && <Modal3 />}
    </>
  );
}

Cada vez mais extenso... Mas claro que há outras maneiras de se fazer isso, da forma acima talvez seja a mais "popular" pela simplicidade.

OK, como posso fazer diferente?

O ponto é que conseguimos ter o mesmo resultado de uma forma mais simples, sem ter que controlar múltiplos states ou ficar fazendo diversas condições.

Primeiro, criamos um componente para controlar a fila de exibição:

// queue.tsx

import { cloneElement, ReactElement, useState } from "react";

type Props = {
  components: ReactElement[];
}

export default function QueueComponents({ components }: Props) {
  const [queue, setQueue] = useState<ReactElement[]>(components);
  const [current, setCurrent] = useState<ReactElement | null>(components?.[0] ?? null);

  function processQueue() { 
    const newQueue = queue.slice(1);
    setQueue(newQueue);
    setCurrent(newQueue[0] ?? null);
  };

  if (!current) return null;

  return cloneElement(current, { processQueue });
}

Basicamente a função dele é inserir os componentes dentro de uma fila (state) e controlar quem deve ser renderizado (de acordo com a ordem inserida). Para a renderização, clonamos o componente filho acrescentando mais uma prop processQueue, para servir de callback.

Conseguimos também aprimorar nosso componente para saber quando não há mais componentes na fila e executar uma ação.

function processQueue() { 
  const newQueue = queue.slice(1);

  if (!newQueue.length) {
    console.log('fila processada, faça algo...');
    
    setQueue([]);
    setCurrent( null);
    return;
  }

  setQueue(newQueue);
  setCurrent(newQueue[0]);
};

Com isso, os componentes que são passados para a fila, conseguem acessar essa nova prop:

// modal.tsx

function Modal({ processQueue }) {
  function onClose() {
    processQueue();
  }

  return (
    <div className="modal">
      <button onClick={onClose}>Fechar modal</button>
    </div>
  )
}

E no arquivo que chama os componentes simplificamos desta forma:

// app.tsx

function App() {
  return (
    <QueueComponents
      components={
        [
          <Modal1 {...props1} />, 
          <Modal2 {...props2} />, 
          <Moda3 {...props3} />
        ]
      }
    />
  )
}

Pronto, fácil né? Isso também pode ser utilizado em outros casos, como por exemplo um Tour (apresentação) de elementos em uma página.

Desta forma conseguimos sempre ter uma ordem de exibição para os componentes, assim não causando múltiplas renderizações desnecessárias.

🙂

Gosto da abordagem, ela demonstra como o React abraça a composição, não se restringindo à componentes de apresentação. Componentes como QueueComponents implementam a lógica para manipular a renderização.

É possível simplificar o JSX para algo parecido com:

function App() {
  return (
    <QueueComponents>
      <Modal1 />
      <Modal2 />
    </QueueComponents>
  );
}

Perceba que children já é uma coleção de ReactElement, logo QueueComponents pode tirar vantagem disso:

type Props = {
  children: ReactElement[];
};

function QueueComponents({ children }: Props) {
  const [queue, setQueue] = useState<ReactElement[]>(children);
  const [current, setCurrent] = useState<ReactElement | null>(
    children?.[0] ?? null
  );

  function processQueue() {
    const newQueue = queue.slice(1);
    setQueue(newQueue);
    setCurrent(newQueue[0] ?? null);
  }

  if (!current) return null;

  return cloneElement(current, { processQueue });
}

Vale mencionar React.Children.toArray(children) tende a ser melhor opção para o caso de uma manipulação mais complexa de children

Para mais informações sobre a propriedade children:

Muito bom! @willieoliveira