Componentes Resistentes em React: Blueprint em Segurança de SSR

Publicado por:

Categorias:

A maioria dos componentes é construída para o cenário ideal. Eles funcionam — até que param de funcionar. O mundo real é hostil. Renderização no servidor. Hidratação. Instâncias múltiplas. Renderização simultânea. Componentes filhos assíncronos. Portais… Seu componente pode enfrentar todos esses desafios. A questão é se ele sobreviverá.

O verdadeiro teste não é se o seu componente funciona na página atual. É se ele funciona quando outra pessoa o utiliza — em condições para as quais você não previu. É aí que os componentes frágeis falham.

Eis como fazê-lo sobreviver .

Dica de Leitura: Agora que você está explorando como tornar seus componentes mais robustos e à prova de falhas, é um ótimo momento para pensar em como otimizar seu fluxo de trabalho de desenvolvimento. Uma ferramenta que pode ajudar nisso é o OpenAI Codex, que pode aumentar sua produtividade como desenvolvedor. Descubra como usar o OpenAI Codex com mais eficiência para melhorar suas habilidades de codificação e tornar seu trabalho mais eficaz.

Torne-o à prova de servidor.

Um provedor de temas simples que lê as preferências do usuário a partir de localStorage:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(
    localStorage.getItem('theme') || 'light'
  )

  return <div className={theme}>{children}</div>
}

Falhas no SSR — lê o tema do localStorage

Mas localStoragenão existe no servidor. No Next.js, Remix ou qualquer framework SSR, isso causa uma falha na compilação. Mova as APIs do navegador para 
useEffect:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    setTheme(localStorage.getItem('theme') || 'light')
  }, [])

  return <div className={theme}>{children}</div>
}

Agora ele é renderizado no servidor sem travar.

Torne-o à prova de hidratação.

Eu também chamo isso de à prova d’água. A versão segura para servidor funciona, mas os usuários veem um flash. O servidor renderiza light, o cliente carrega, então o efeito é executado e muda para dark:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    setTheme(localStorage.getItem('theme') || 'light')
  }, [])

  return <div className={theme}>{children}</div>
}

Aqui, flash de tema errado — useEffect é executado após a hidratação

Injete um script síncrono que defina o valor correto 
antes que o navegador renderize a página e o React seja carregado. O DOM já terá a classe correta quando o React assumir o controle.

function ThemeProvider({ children }) {
  return (
    <>
      <div id="theme">{children}</div>
      <script dangerouslySetInnerHTML={{ __html: `
        try {
          const theme = localStorage.getItem('theme') || 'light'
          document.getElementById('theme').className = theme
        } catch (e) {}
      `}} />
    </>
  )
}

Aqui, o script embutido define o tema antes que o navegador renderize a imagem.

Sem incompatibilidade, sem flash.

Torne-o à prova de instâncias

A versão à prova de hidratação tem como alvo um valor fixo . Mas e se alguém usar dois valores fixos?id="theme"ThemeProvider

function App() {
  return (
    <>
      <ThemeProvider><MainContent /></ThemeProvider>
      <AlwaysLightThemeContent />
      <ThemeProvider><Sidebar /></ThemeProvider>
    </>
  )
}

Aqui, múltiplas instâncias — ambos os scripts têm como alvo o mesmo ID.

Ambos os scripts disputam o mesmo elemento. Use useIdpara gerar IDs estáveis ​​e exclusivos por instância:

function ThemeProvider({ children }) {
  const id = useId()
  return (
    <>
      <div id={id}>{children}</div>
      <script dangerouslySetInnerHTML={{ __html: `
        try {
          const theme = localStorage.getItem('theme') || 'light'
          document.getElementById('${id}').className = theme
        } catch (e) {}
      `}} />
    </>
  )
}

Agora, várias instâncias coexistem em segurança.

Torne-o à prova de concorrência

Agora vamos tornar o tema controlado pelo servidor. Um componente de servidor que busca as preferências do usuário:

async function ThemeProvider({ children }) {
  const prefs = await db.preferences.get(userId)

  return <div className={prefs.theme}>{children}</div>
}

Assim como antes, renderizar a consulta em dois locais diferentes pode resultar em duas consultas idênticas ao banco de dados. Para eliminar as duplicatas em uma única requisição, envolva a consulta em um bloco `try-catch`.React.cache

import { cache } from 'react'

const getPreferences = cache(
  userId => db.preferences.get(userId)
)

async function ThemeProvider({ children }) {
  const prefs = await getPreferences(userId)

  return <div className={prefs.theme}>{children}</div>
}

Nota lateral:O cache() do React remove chamadas simultâneas duplicadas.

A mesma consulta, executada de qualquer lugar, acessa o banco de dados apenas uma vez.

Torne-o à prova de contaminação

Às vezes, você deseja passar dados para os filhos como props, o que tradicionalmente significava usar :React.cloneElement

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  return React.Children.map(children, (child) => {
    return React.cloneElement(child, { theme })
  })
}

Nota lateral:Passa o tema para os filhos através do cloneElement.

Mas com componentes de servidor React , ` this` ou `react-server` podem ser uma Promise ou uma referência opaca — não funcionarão. Use o contexto em vez disso:React.lazy"use cache"childrencloneElement

const ThemeContext = createContext('light')

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  )
}

Nota lateral:O contexto funciona em todos os lugares — servidor, cliente, assíncrono

As crianças leem o tema do início ao fim useContext— sem perfuração de adereços, sem clonagem.

Torne-o à prova de portais.

Um provedor de temas com um atalho de teclado para ativar/desativar o modo escuro:Cmd+D

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    const toggle = (e) => {
      if (e.metaKey && e.key === 'd') {
        e.preventDefault()
        setTheme(t => t === 'dark' ? 'light' : 'dark')
      }
    }
    window.addEventListener('keydown', toggle)
    return () => window.removeEventListener('keydown', toggle)
  }, [])

  return <div className={theme}>{children}</div>
}

Nota lateral:Atalho de teclado global para alternar o tema

Mas se alguém renderizar o aplicativo dentro de uma janela pop-up, iframe ou via createPortal, o atalho para de funcionar. O ouvinte está associado ao componente pai window, não ao componente em que seu componente está inserido. Use :ownerDocument.defaultView

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const ref = useRef(null)

  useEffect(() => {
    const win = ref.current?.ownerDocument.defaultView || window
    const toggle = (e) => {
      if (e.metaKey && e.key === 'd') {
        e.preventDefault()
        setTheme(t => t === 'dark' ? 'light' : 'dark')
      }
    }
    win.addEventListener('keydown', toggle)
    return () => win.removeEventListener('keydown', toggle)
  }, [])

  return <div ref={ref} className={theme}>{children}</div>
}

Nota lateral:ownerDocument.defaultView encontra a janela correta.

Agora o atalho funciona em qualquer contexto de janela.

Torne-o à prova de transições

Um painel de configurações que alterna entre visualizações simples e avançadas:

function ThemeSettings() {
  const [showAdvanced, setShowAdvanced] = useState(false)

  return (
    <>
      {showAdvanced ? <AdvancedPanel /> : <SimplePanel />}
      <button onClick={() => setShowAdvanced(!showAdvanced)}>
        {showAdvanced ? 'Simple' : 'Advanced'}
      </button>
    </>
  )
}

Nota lateral:Alternância simples entre dois painéis.

Envolva-o em um componente do React 19 e nada será animado — os painéis simplesmente se encaixarão. As atualizações de estado devem passar por :<ViewTransition>startTransition

function ThemeSettings() {
  const [showAdvanced, setShowAdvanced] = useState(false)

  return (
    <>
      {showAdvanced ? <AdvancedPanel /> : <SimplePanel />}
      <button onClick={() =>
        startTransition(() => setShowAdvanced(!showAdvanced))
      }>
        {showAdvanced ? 'Simple' : 'Advanced'}
      </button>
    </>
  )
}

Nota lateral:startTransition habilita a transição de visualização

Agora a transição ocorre de forma suave.

Torne-o à prova de atividades

Um componente de tema que injeta variáveis ​​CSS através de uma tag:<style>

function DarkTheme({ children }) {
  return (
    <>
      <style>{`
        :root {
          --bg: #000;
          --fg: #fff;
        }
      `}</style>
      {children}
    </>
  )
}

Nota lateral:Injeta variáveis ​​CSS globais através da tag de estilo.

Mas se você envolver o elemento em `<div>` , o tema escuro persiste mesmo quando oculto. `<div>` preserva o DOM e tem efeitos colaterais em nível de DOM — ele modifica variáveis ​​globalmente. O React não consegue limpar esses efeitos colaterais automaticamente. Defina ` disable-styles` para desativar os estilos quando o elemento estiver oculto:<Activity><Activity><style>:rootmedia="not all"

function DarkTheme({ children }) {
  const ref = useRef(null)

  useLayoutEffect(() => {
    if (!ref.current) return
    ref.current.media = 'all'
    return () => ref.current.media = 'not all'
  }, [])

  return (
    <>
      <style ref={ref}>{`
        :root {
          --bg: #000;
          --fg: #fff;
        }
      `}</style>
      {children}
    </>
  )
}

Nota lateral:useLayoutEffect define media=’not all’ quando oculto e restaura-o quando exibido novamente.

Agora, os componentes ocultos não terão o tema escuro aplicado.

Torne-o à prova de vazamentos

Um componente de servidor que passa um userobjeto (incluindo um token de sessão) para outro componente de tema. Caso de uso válido: você precisa dos dados no servidor. Você provavelmente sabe UserThemeConfigque se trata de um componente de servidor e que é seguro passar os dados para ele.

async function Dashboard() {
  const user = await getUser()

  return <UserThemeConfig user={user} />
}

Nota lateral:O painel de controle encaminha o usuário (com o token) para outro componente.

No entanto, você não conhece UserThemeConfigo comportamento exato do componente, o que ele renderiza ou o que uma versão futura poderá fazer. Você não é responsável pela manutenção dele.

Além disso, como o componente UserThemeConfignão cria um objeto user, ele pode não saber que o usertoken possui uma propriedade sensível token. Você não controla esse componente, portanto, não pode presumir que ele não passará esse token para um componente cliente em algum lugar em sua árvore de componentes. O token é serializado e enviado para o cliente. Use o recurso experimental do React taintUniqueValuepara marcar o token como exclusivo do servidor. Se esse valor for passado para um componente cliente, o React lançará uma exceção. Para bloquear um objeto inteiro em vez de um único valor, use ` taintObjectReference.

import { experimental_taintUniqueValue } from 'react'

async function Dashboard() {
  const user = await getUser()

  experimental_taintUniqueValue(
    'Do not pass the user token to the client.',
    user,
    user.token
  )

  return <UserThemeConfig user={user} />
}

Nota lateral:O parâmetro `taintUniqueValue` impede que o token do usuário seja enviado ao cliente.

Se o código desse componente (ou uma futura refatoração feita por outro membro da equipe) tentar passar o token para um componente cliente, o React lançará uma exceção com a sua mensagem. O caso de uso válido permanece; o token nunca vaza.user.token

Prepare-o para o futuro *

É importante entender o conceito de ser defensivo. Não se trata de um padrão que se aplica a todas as situações.

Um tema que gera cores de destaque aleatórias na tela:

function ThemeProvider({ baseTheme, children }) {
  const colors = useMemo(
    () => getRandomColors(baseTheme),
    [baseTheme]
  )

  return <div style={colors}>{children}</div>
}

Nota lateral:useMemo armazena em cache as cores geradas.

Mas useMemoisso é uma dica de desempenho, não uma garantia semântica . O React descarta valores em cache durante a atualização de memória de alto nível (HMR) e reserva-se o direito de fazê-lo para componentes fora da tela ou recursos que ainda não existem. Se o React descartar o cache, seu tema piscará com cores diferentes. Use o estado quando a correção depender da persistência.

function ThemeProvider({ baseTheme, children }) {
  const [colors, setColors] = useState(() => generateAccentColors(baseTheme))
  const [prevTheme, setPrevTheme] = useState(baseTheme)

  if (baseTheme !== prevTheme) {
    setPrevTheme(baseTheme)
    setColors(generateAccentColors(baseTheme))
  }

  return <div style={colors}>{children}</div>
}

Nota lateral:useState fornece garantia de persistência semântica

Agora as cores permanecem estáveis ​​independentemente das otimizações internas do React.


Esses não são casos extremos. São o novo normal. Os componentes que quebram? Eles não eram frágeis. Foram construídos para o React de ontem. Estamos construindo para o de amanhã.


✦ Recomendação do Editor

Eleve o seu nível no assunto

Se você está procurando aprender mais sobre componentes resistentes em React após ler nosso artigo sobre como tornar seus componentes mais robustos e à prova de falhas, eu recomendo procurar por ‘React para desenvolvedores avançados’ na Amazon.

Adquirir conhecimento sobre componentes resistentes em React é essencial para qualquer desenvolvedor que deseje criar soluções escaláveis e robustas. Com essa habilidade, você poderá conquistar seu mercado e superar os concorrentes. Além disso, estudar em profundidade sobre este tema te dará uma visão mais completa e profunda para a carreira, permitindo-lhe abordar projetos complexos com confiança e criatividade



Ver ofertas em destaque na Amazon


Ajude a manter este projeto, a Ramos da Informática pode ganhar uma comissão sobre as vendas qualificadas.
Ramos Souza J
Ramos Souza Jhttps://ramosdainformatica.com.br/sobre/
Com mais de 26 anos de experiência em desenvolvimento de software, minha carreira é marcada por constante evolução tecnológica e pela entrega de soluções que fazem a diferença. Desde os primeiros passos com Clipper e Delphi até a consolidação em JavaScript e TypeScript, desenvolvi expertise em frameworks como Node.js, Nest e React, além de bancos de dados relacionais e não relacionais. Sou um Desenvolvedor Full Stack apaixonado por resolver problemas complexos com excelência técnica, adaptando-me rapidamente a novos desafios. Além do domínio técnico, sou reconhecido por meu relacionamento interpessoal e compromisso com resultados. Atualmente, trabalho em uma startup de Health-Tech e sou voluntário na OpenJS Foundation, promovendo o avanço do ecossistema JavaScript. Além de manter este site.

Leia mais

Artigos relacionados