DEV Community

Cover image for De Next.js a React Edge com Cloudflare Workers: Uma História de Libertação
Felipe Rohde
Felipe Rohde

Posted on

4 1

De Next.js a React Edge com Cloudflare Workers: Uma História de Libertação

A Gota D'água

Tudo começou com uma fatura da Vercel. Não, na verdade começou bem antes - com pequenas frustrações que foram se acumulando. A necessidade de pagar por features básicas como proteção DDoS, logs mais detalhados, ou mesmo um firewall decente, filas de builds, etc. A sensação de estar preso em um vendor lock-in cada vez mais caro.

"E o pior de tudo: nossos preciosos cabeçalhos de SEO simplesmente deixaram de ser renderizados no servidor em uma aplicação utilizando o pages router. Uma verdadeira dor de cabeça para qualquer dev! 😭"

Mas o que realmente me fez repensar tudo foi a direção que o Next.js estava tomando. A introdução do use client, use server - diretivas que, em teoria, deveriam simplificar o desenvolvimento, mas na prática adicionavam mais uma camada de complexidade para gerenciar. Era como se estivéssemos voltando aos tempos do PHP, marcando arquivos com diretivas para dizer onde eles deveriam rodar.

E não para por aí. O App Router, uma ideia interessante, mas implementada de forma que criou um framework praticamente novo dentro do Next.js. De repente, tínhamos duas formas completamente diferentes de fazer a mesma coisa. A 'velha' e a 'nova' - com comportamentos sutilmente diferentes e armadilhas escondidas.

A Alternativa com Cloudflare 😍

Foi quando percebi: por que não aproveitar a incrível infraestrutura da Cloudflare com Workers rodando no edge, R2 para storage, KV para dados distribuídos... Além, é claro, a incrível proteção DDoS, CDN global, firewall, regras par páginas e rotas e tudo mais que a Cloudflare oferece.

E o melhor: um modelo de preço justo, onde você paga pelo que usa, sem surpresas.

Assim nasceu o React Edge. Um framework que não tenta reinventar a roda, mas sim proporcionar uma experiência de desenvolvimento verdadeiramente simples e moderna.

React Edge: O Framework React derivado de todas (ou quase) as dores de um desenvolvedor

Quando comecei a desenvolver o React Edge, tinha um objetivo claro: criar um framework que fizesse sentido. Não mais lutar com diretivas confusas, não mais pagar fortunas por recursos básicos, e principalmente, não mais ter que lidar com a complexidade artificial criada pela separação cliente/servidor. Eu queria velocidade, algo que entregasse performance sem sacrificar simplicidade. Aproveitando meu conhecimento da API do React e anos como desenvolvedor Javascript e Golang, sabia exatamente como lidar com streams e multiplexação para otimizar a renderização e o gerenciamento de dados.

O Cloudflare Workers, com sua infraestrutura poderosa e presença global, me ofereceu o ambiente perfeito para explorar essas possibilidades. Queria algo que fosse verdadeiramente híbrido, e essa combinação de ferramentas e experiência foi o que deu vida ao React Edge: um framework que resolve problemas reais com soluções modernas e eficientes.

O React Edge traz uma abordagem revolucionária para desenvolvimento React. Imagine poder escrever uma classe no servidor e chamá-la diretamente do cliente, com tipagem completa e zero configuração. Imagine um sistema de cache distribuído que "simplesmente funciona", permitindo invalidação por tags ou prefixos. Imagine poder compartilhar estado entre servidor e cliente de forma transparente e segura. Além de simplificar a autenticação e trazer uma abordagem de internacionalização eficiente, CLI e muito mais.

Sua comunicação RPC é tão natural que parece mágica - você escreve métodos em uma classe e os chama do cliente como se fossem locais. O sistema de multiplexação inteligente garante que, mesmo que múltiplos componentes façam a mesma chamada, apenas uma requisição seja feita ao servidor. O cache efêmero evita requisições repetidas desnecessárias, e tudo isso funciona tanto no servidor quanto no cliente.

Um dos pontos mais poderosos é o hook app.useFetch, que unifica a experiência de data fetching. No servidor, ele pré-carrega os dados durante o SSR; no cliente, ele hidrata automaticamente com esses dados e permite atualizações sob demanda. E com suporte a polling automático e reatividade baseada em dependências, criar interfaces dinâmicas nunca foi tão fácil.

Mas não para por aí. O framework oferece um sistema de rotas poderoso (inspirado no fantástico Hono), gerenciamento de assets integrado com Cloudflare R2, e uma forma elegante de lidar com erros através da classe HttpError. Os middlewares podem facilmente enviar dados para o cliente através de um store compartilhado, e tudo é ofuscado automaticamente para segurança.

O mais impressionante? Quase todo o código do framework é híbrido. Não há uma versão 'cliente' e outra 'servidor' - o mesmo código funciona nos dois ambientes, adaptando-se automaticamente ao contexto. O cliente recebe apenas o que precisa, tornando o bundle final extremamente otimizado.

E a cereja do bolo: tudo isso roda na infraestrutura edge do Cloudflare Workers, proporcionando performance excepcional a um custo justo. Sem surpresas na fatura, sem recursos básicos escondidos atrás de planos enterprise forçados, apenas um framework sólido que permite você focar no que realmente importa: criar aplicações incríveis. Além disso, o React Edge aproveita todo o ecossistema da Cloudflare, incluindo Queues, Durable Objects, KV Storage, e muito mais para oferecer uma base robusta e escalável para suas aplicações.

O Vite foi usado como base, tanto para o ambiente de desenvolvimento quanto para testes e build. O Vite, com sua velocidade impressionante e arquitetura moderna, permite um fluxo de trabalho ágil e eficiente. Ele não apenas acelera o desenvolvimento, mas também otimiza o processo de build, garantindo que o código seja compilado de forma rápida e precisa. Sem dúvida, o Vite foi a escolha perfeita para o React Edge.

Repensando o Desenvolvimento React para a era do Edge Computing

Você já se perguntou como seria desenvolver aplicações React sem se preocupar com a barreira cliente/servidor? Sem precisar decorar dezenas de diretivas como use client ou use server? E melhor ainda: e se você pudesse chamar funções do servidor como se fossem locais, com tipagem completa e zero configuração?

Com o React Edge, você não precisa:

  • Criar rotas de API separadas
  • Gerenciar estado de loading/error manualmente
  • Implementar debounce na mão
  • Se preocupar com serialização/deserialização
  • Lidar com CORS
  • Gerenciar tipagem entre cliente/servidor
  • Lidar com regras de autenticação manualmente
  • Gerenciar como a internacionalização é feita

E o melhor: tudo isso funciona tanto no servidor quanto no cliente, sem precisar marcar nada com use client ou use server. O framework sabe o que fazer baseado no contexto. Vamos lá?

A Magia do RPC Tipado

Imagine poder fazer isso:

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
Enter fullscreen mode Exit fullscreen mode

Compare isso com o Next.js/Vercel:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... resto do componente
}
Enter fullscreen mode Exit fullscreen mode

O Poder do useFetch: Onde a Mágica Acontece

Data Fetching Repensado

Esqueça tudo que você sabe sobre data fetching no React. O app.useFetch do React Edge traz uma abordagem completamente nova e poderosa. Imagine um hook que:

  • Pré-carrega dados no servidor durante SSR
  • Hidrata automaticamente no cliente sem flicker
  • Mantém tipagem completa entre cliente e servidor
  • Suporta reatividade com debounce inteligente
  • Multiplexa chamadas idênticas automaticamente
  • Permite atualizações programáticas e polling

Vamos ver isso em ação:

// Primeiro, definimos nossa API no servidor
class PropertiesAPI extends Rpc {
 async searchProperties(filters: PropertyFilters) {
   const results = await this.db.properties.search(filters);
   // Cache automático por 5 minutos
   return this.createResponse(results, {
     cache: { ttl: 300, tags: ['properties'] }
   });
 }

 async getPropertyDetails(ids: string[]) {
   return Promise.all(
     ids.map(id => this.db.properties.findById(id))
   );
 }
}

// Agora, no cliente, a mágica acontece
const PropertySearch = () => {
 const [filters, setFilters] = useState<PropertyFilters>({
   price: { min: 100000, max: 500000 },
   bedrooms: 2
 });

 // Busca reativa com debounce inteligente
 const { 
   data: searchResults,
   loading: searchLoading,
   error: searchError
 } = app.useFetch(
   async (ctx) => ctx.rpc.searchProperties(filters),
   {
     // Quando filters muda, refaz a busca
     deps: [filters],
     // Mas espera 300ms de 'silêncio' antes de buscar
     depsDebounce: {
       filters: 300
     }
   }
 );

 // Agora, vamos buscar os detalhes das propriedades encontradas
 const {
   data: propertyDetails,
   loading: detailsLoading,
   fetch: refreshDetails
 } = app.useFetch(
   async (ctx) => {
     if (!searchResults?.length) return null;

     // Isso parece fazer múltiplas chamadas, mas...
     return ctx.rpc.batch([
       // Na verdade, tudo é multiplexado em uma única requisição!
       ...searchResults.map(result => 
         ctx.rpc.getPropertyDetails(result.id)
       )
     ]);
   },
   {
     // Atualiza sempre que searchResults mudar
     deps: [searchResults]
   }
 );

 // Interface bonita e responsiva
 return (
   <div>
     <FiltersPanel 
       value={filters}
       onChange={setFilters}
       disabled={searchLoading}
     />

     {searchError && (
       <Alert status='error'>
         Erro na busca: {searchError.message}
       </Alert>
     )}

     <PropertyGrid
       items={propertyDetails || []}
       loading={detailsLoading}
       onRefresh={() => refreshDetails()}
     />
   </div>
 );
};
Enter fullscreen mode Exit fullscreen mode

A Mágica da Multiplexação

O exemplo acima esconde uma característica poderosa: a multiplexação inteligente. Quando você usa ctx.rpc.batch, o React Edge não apenas agrupa as chamadas - ele deduplicar chamadas idênticas automaticamente:

const PropertyListingPage = () => {
  const { data } = app.useFetch(async (ctx) => {
    // Mesmo que você faça 100 chamadas idênticas...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // mesma chamada
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // mesma chamada
    ]);
  });

  // Mas na realidade:
  // 1. O batch agrupa todas as chamadas em UMA única requisição HTTP
  // 2. Chamadas idênticas são deduplicas automaticamente
  // 3. O resultado é distribuído corretamente para cada posição do array
  // 4. A tipagem é mantida para cada resultado individual!


  // Entao..
  // 1. getProperty('123')
  // 2. getProperty('456')
  // E os resultados são distribuídos para todos os chamadores!
};
Enter fullscreen mode Exit fullscreen mode

SSR + Hidratação Perfeita

Uma das partes mais impressionantes é como o useFetch lida com SSR:

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Controle fino de quando executar
      shouldFetch: ({ worker, loaded }) => {
        // No worker (SSR): sempre busca
        if (worker) return true;
        // No cliente: só busca se não tiver dados
        return !loaded;
      }
    }
  );

  // No servidor:
  // 1. useFetch faz a chamada RPC
  // 2. Dados são serializados e enviados ao cliente
  // 3. Componente renderiza com os dados

  // No cliente:
  // 1. Componente hidrata com os dados do servidor
  // 2. Não faz nova chamada (shouldFetch retorna false)
  // 3. Se necessário, pode refazer a chamada com data.fetch()

  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductView 
        product={data}
        loading={loading}
        error={error}
      />
    </Suspense>
  );
};
Enter fullscreen mode Exit fullscreen mode

Além do useFetch: O arsenal completo

RPC: A Arte da Comunicação Cliente-Servidor

Segurança e Encapsulamento

O sistema RPC do React Edge foi projetado pensando em segurança e encapsulamento. Nem tudo que está em uma classe RPC é automaticamente exposto ao cliente:

class PaymentsAPI extends Rpc {
 // Propriedades nunca são expostas
 private stripe = new Stripe(process.env.STRIPE_KEY);

 // Métodos começando com $ são privados
 private async $validateCard(card: CardInfo) {
   return await this.stripe.cards.validate(card);
 }

 // Métodos começando com _ também são privados
 private async _processPayment(amount: number) {
   return await this.stripe.charges.create({ amount });
 }

 // Este método é público e acessível via RPC
 async createPayment(orderData: OrderData) {
   // Validação interna usando método privado
   const validCard = await this.$validateCard(orderData.card);
   if (!validCard) {
     throw new HttpError(400, 'Invalid card');
   }

   // Processamento usando outro método privado
   const payment = await this._processPayment(orderData.amount);
   return payment;
 }
}

// No cliente:
const PaymentForm = () => {
 const { rpc } = app.useContext<App.Context>();

 // ✅ Isso funciona
 const handleSubmit = () => rpc.createPayment(data);

 // ❌ Isso não é possível - métodos privados não são expostos
 const invalid1 = () => rpc.$validateCard(data);
 const invalid2 = () => rpc._processPayment(100);

 // ❌ Isso também não funciona - propriedades não são expostas
 const invalid3 = () => rpc.stripe;
};
Enter fullscreen mode Exit fullscreen mode

Hierarquia de APIs RPC

Uma das características mais poderosas do RPC é a capacidade de organizar APIs em hierarquias:

// APIs aninhadas para melhor organização
class UsersAPI extends Rpc {
  // Subclasse para gerenciar preferences
  preferences = new UserPreferencesAPI();
  // Subclasse para gerenciar notificações
  notifications = new UserNotificationsAPI();

  async getProfile(id: string) {
    return this.db.users.findById(id);
  }
}

class UserPreferencesAPI extends Rpc {
  async getTheme(userId: string) {
    return this.db.preferences.getTheme(userId);
  }

  async setTheme(userId: string, theme: Theme) {
    return this.db.preferences.setTheme(userId, theme);
  }
}

class UserNotificationsAPI extends Rpc {
  // Métodos privados continuam privados
  private async $sendPush(userId: string, message: string) {
    await this.pushService.send(userId, message);
  }

  async getSettings(userId: string) {
    return this.db.notifications.getSettings(userId);
  }

  async notify(userId: string, notification: Notification) {
    const settings = await this.getSettings(userId);
    if (settings.pushEnabled) {
      await this.$sendPush(userId, notification.message);
    }
  }
}

// No cliente:
const UserProfile = () => {
  const { rpc } = app.useContext<App.Context>();

  const { data: profile } = app.useFetch(
    async (ctx) => {
      // Chamadas aninhadas são totalmente tipadas
      const [user, theme, notificationSettings] = await ctx.rpc.batch([
        // Método da classe principal
        ctx.rpc.getProfile('123'),
        // Método da subclasse de preferências
        ctx.rpc.preferences.getTheme('123'),
        // Método da subclasse de notificações
        ctx.rpc.notifications.getSettings('123')
      ]);

      return { user, theme, notificationSettings };
    }
  );

  // ❌ Métodos privados continuam inacessíveis
  const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};
Enter fullscreen mode Exit fullscreen mode

Benefícios da Hierarquia

Organizar APIs em hierarquias traz vários benefícios:

  • Organização Lógica: Agrupe funcionalidades relacionadas de forma intuitiva
  • Namespace Natural: Evite conflitos de nomes com caminhos claros (users.preferences.getTheme)
  • Encapsulamento: Mantenha métodos auxiliares privados em cada nível
  • Manutenibilidade: Cada subclasse pode ser mantida e testada independentemente
  • Tipagem Completa: O TypeScript entende toda a hierarquia

O sistema de RPC do React Edge torna a comunicação cliente-servidor tão natural que você quase esquece que está fazendo chamadas remotas. E com a capacidade de organizar APIs em hierarquias, você pode criar estruturas complexas mantendo o código organizado e seguro.

Um Sistema de i18n que Faz Sentido

O React Edge traz um sistema de internacionalização elegante e flexível, que suporta interpolação de variáveis e formatação complexa sem bibliotecas pesadas.

// translations/fr.ts
export default {
  'Good Morning, {name}!': 'Bonjour, {name}!',
};
Enter fullscreen mode Exit fullscreen mode

Uso no código:

const WelcomeMessage = () => {
  const userName = 'João';

  return (
    <div>
      {/* Output: Bem vindo, João! */}
      <h1>{__('Good Morning, {name}!', { name: userName })}</h1>
  );
};
Enter fullscreen mode Exit fullscreen mode

Configuração Zero

O React Edge detecta e carrega suas traduções automaticamente, podendo salvar fácilmente nos cookies a preferência do usuário. Mas isso você já esperava, certo?

// worker.ts
const handler = {
    fetch: async (request: Request, env: types.Worker.Env, context: ExecutionContext) => {
        const url = new URL(request.url);

        const lang = (() => {
            const lang =
                url.searchParams.get('lang') || worker.cookies.get(request.headers, 'lang') || request.headers.get('accept-language') || '';

            if (!lang || !i18n[lang]) {
                return 'en-us';
            }

            return lang;
        })();

        const worker = new AppWorkerEntry({
            i18n: {
                en: await import('./translations/en'),
                pt: await import('./translations/pt'),
                es: await import('./translations/es')
            }
        });

        const res = await workerApp.fetch();

        if (url.searchParams.has('lang')) {
            return new Response(res.body, {
                headers: worker.cookies.set(res.headers, 'lang', lang)
            });
        }

        return res;
    }
};
Enter fullscreen mode Exit fullscreen mode

Autenticação JWT que "Simplesmente Funciona"

A autenticação sempre foi um ponto de dor em aplicações web. Gerenciar tokens JWT, cookies seguros, revalidação - tudo isso geralmente requer muito código boilerplate. O React Edge muda isso completamente.

Veja como é simples implementar um sistema completo de autenticação:

class SessionAPI extends Rpc {
 private auth = new AuthJwt({
   // Cookie será automaticamente gerenciado
   cookie: 'token',
   // Payload é automaticamente encriptado
   encrypt: true,
   // Expiração automática
   expires: { days: 1 },
   secret: process.env.JWT_SECRET
 });

 async signin(credentials: { 
   email: string; 
   password: string 
 }) {
   // Validação com Zod
   const validated = loginSchema.parse(credentials);
   const { headers } = await this.auth.sign(validated));

   // Retorna resposta com cookies configurados
   return this.createResponse(
     { email: validated.email },
     {
       headers: (await this.auth.sign(validated))
     }
   );
 }

 async getSession(revalidate = false) {
   // Validação e revalidação automática de token
   const { headers, payload } = await this.auth.authenticate(
     this.request.headers,
     revalidate
   );

   return this.createResponse(payload, { headers });
 }

 async signout() {
   // Limpa cookies automaticamente
   const { headers } = await this.auth.destroy();
   return this.createResponse(null, { headers });
 }
}
Enter fullscreen mode Exit fullscreen mode

Uso no Cliente: Zero Configuração

const LoginForm = () => {
  const { rpc } = app.useContext<App.Context>();

  const login = async (values) => {
    const session = await rpc.signin(values);
    // Pronto! Cookies já estão setados automaticamente
  };

  return <Form onSubmit={login}>...</Form>;
};

const NavBar = () => {
  const { rpc } = app.useContext<App.Context>();

  const logout = async () => {
    await rpc.signout();
    // Cookies já foram limpos automaticamente
  };

  return <button onClick={logout}>Sair</button>;
};
Enter fullscreen mode Exit fullscreen mode

Por Que Isso é Revolucionário?

  1. Zero Boilerplate

    • Sem gerenciamento manual de cookies
    • Sem necessidade de interceptors
    • Sem refresh tokens manual
  2. Segurança por Padrão

    • Tokens são automaticamente encriptados
    • Cookies são seguros e httpOnly
    • Revalidação automática
  3. Tipagem Completa

    • Payload do JWT é tipado
    • Validação com Zod integrada
    • Erros de autenticação tipados
  4. Integração Perfeita

// Middleware que protege rotas
const authMiddleware: App.Middleware = async (ctx) => {
  const session = await ctx.rpc.session.getSession();

  if (!session) {
    throw new HttpError(401, 'Unauthorized');
  }

  // Disponibiliza sessão para componentes
  ctx.store.set('session', session, 'public');
};

// Uso em rotas
const router: App.Router = {
  routes: [
    routerBuilder.routeGroup({
      path: '/dashboard',
      middlewares: [authMiddleware],
      routes: [/*...*/]
    })
  ]
};
Enter fullscreen mode Exit fullscreen mode

O Store Compartilhado

Uma das features mais poderosas do React Edge é sua capacidade de compartilhar estado entre worker e cliente de forma segura. Vamos ver como isso funciona:

// middleware/auth.ts
const authMiddleware: App.Middleware = async (ctx) => {
  const token = ctx.request.headers.get('authorization');

  if (!token) {
    throw new HttpError(401, 'Unauthorized');
  }

  const user = await validateToken(token);

  // Dados públicos - automaticamente compartilhados com o cliente
  ctx.store.set('user', {
    id: user.id,
    name: user.name,
    role: user.role
  }, 'public');

  // Dados privados - permanecem apenas no worker mas
  ctx.store.set('userSecret', user.secret);
};

// components/Header.tsx
const Header = () => {
  // Acesso transparente aos dados do store
  const { store } = app.useContext();
  const user = store.get('user');

  return (
    <header>
      <h1>Bem vindo, {user.name}!</h1>
      {user.role === 'admin' && (
        <AdminPanel />
      )}
    </header>
  );
};
Enter fullscreen mode Exit fullscreen mode

Como Funciona

  • Dados Públicos: Os dados marcados como públicos são compartilhados de forma segura com o cliente, tornando-os facilmente acessíveis para os componentes.
  • Dados Privados: Os dados sensíveis permanecem dentro do ambiente do worker e nunca são expostos ao cliente.
  • Integração com Middleware: O middleware pode popular a store com dados públicos e privados, garantindo um fluxo contínuo de informações entre a lógica do servidor e a renderização do lado do cliente.

Benefícios

  1. Segurança: A separação dos escopos de dados públicos e 2. privados garante que as informações sensíveis permaneçam protegidas.
  2. Conveniência: O acesso transparente aos dados da store simplifica o gerenciamento de estado entre worker e cliente.
  3. Flexibilidade: A store é facilmente integrada com middleware, permitindo atualizações dinâmicas de estado baseadas no tratamento de requisições.

Roteamento Elegante

O sistema de rotas do React Edge é inspirado no Hono, mas com superpoderes para SSR:

const router: App.Router = {
  routes: [
    routerBuilder.routeGroup({
      path: '/dashboard',
      // Middlewares aplicados a todas rotas do grupo
      middlewares: [authMiddleware, dashboardMiddleware],
      routes: [
        routerBuilder.route({
          path: '/',
          handler: {
            page: {
              value: DashboardPage,
              // Headers específicos para esta rota
              headers: new Headers({
                'Cache-Control': 'private, max-age=0'
              })
            }
          }
        }),
        routerBuilder.route({
          path: '/api/stats',
          handler: {
            // Rotas podem retornar respostas diretas
            response: async (ctx) => {
              const stats = await ctx.rpc.stats.getDashboardStats();
              return {
                value: Response.json(stats),
                // Cache por 5 minutos
                cache: { ttl: 300 }
              };
            }
          }
        })
      ]
    })
  ]
};
Enter fullscreen mode Exit fullscreen mode

Principais Recursos

  • Rotas Agrupadas: Agrupamento lógico de rotas relacionadas sob um caminho e middleware compartilhados. Manipuladores Flexíveis: Defina manipuladores que retornam páginas ou respostas diretas da API.
  • Cabeçalhos por Rota: Personalize cabeçalhos HTTP para rotas individuais.
  • Cache Integrado: Simplifique estratégias de cache com ttl e tags.

Benefícios

  1. Consistência: Ao agrupar rotas relacionadas, você garante a aplicação consistente de middleware e organização do código.
  2. Escalabilidade: O sistema suporta roteamento aninhado e modular para aplicações em grande escala.
  3. Desempenho: Suporte nativo para cache garante tempos de resposta ideais sem configurações manuais.

Cache Distribuído com Edge Cache

O React Edge possui um sistema de cache poderoso que funciona tanto para dados JSON quanto para páginas inteiras:

class ProductsAPI extends Rpc {
  async getProducts(category: string) {
    const products = await this.db.products.findByCategory(category);

    return this.createResponse(products, {
      cache: {
        ttl: 3600, // 1 hora
        tags: [`category:${category}`, 'products']
      }
    });
  }

  async updateProduct(id: string, data: ProductData) {
    await this.db.products.update(id, data);

    // Invalida cache específico do produto e sua categoria
    await this.cache.deleteBy({
      tags: [
        `product:${id}`,
        `category:${data.category}`
      ]
    });
  }

  async searchProducts(query: string) {
    const results = await this.db.products.search(query);

    // Cache com prefixo para fácil invalidação
    return this.createResponse(results, {
      cache: {
        ttl: 300,
        tags: [`search:${query}`]
      }
    });
  }
}

// Em qualquer lugar do código:
await cache.deleteBy({
  // Invalida todos resultados de busca
  keyPrefix: 'search:',
  // E todos produtos de uma categoria
  tags: ['category:electronics']
});
Enter fullscreen mode Exit fullscreen mode

Principais Recursos

  • Invalidação Baseada em Tags: Entradas de cache podem ser agrupadas usando tags, permitindo invalidação fácil e seletiva quando os dados são alterados.
  • Correspondência por Prefixo: Invalide várias entradas de cache usando um prefixo comum, ideal para cenários como consultas de pesquisa ou dados hierárquicos.
  • Tempo de Vida (TTL): Defina tempos de expiração para entradas de cache para garantir dados atualizados mantendo alto desempenho.

Benefícios

  1. Desempenho Aprimorado: Reduz a carga nas APIs ao fornecer respostas em cache para dados frequentemente acessados.
  2. Escalabilidade: Gerencia eficientemente grandes conjuntos de dados e alto tráfego com um sistema de cache distribuído.
  3. Flexibilidade: Controle refinado sobre o cache, permitindo que desenvolvedores otimizem o desempenho sem sacrificar a precisão dos dados.

Link: O Componente que Pensa à Frente

O componente Link é uma solução inteligente e performática para pré-carregar recursos no lado cliente, garantindo uma navegação mais fluida e rápida para os usuários. Sua funcionalidade de prefetching é ativada ao passar o cursor sobre o link, aproveitando o momento de inatividade do usuário para requisitar antecipadamente os dados do destino.

Como Funciona?

  1. Prefetch Condicional: O atributo prefetch (ativo por padrão) controla se o pré-carregamento será realizado.

  2. Cache Inteligente: Um conjunto (Set) é usado para armazenar os links já pré-carregados, evitando chamadas redundantes.

  3. Mouse Enter: Quando o usuário passa o cursor sobre o link, a função handleMouseEnter verifica se o pré-carregamento é necessário e, caso positivo, inicia uma requisição fetch para o destino.

  4. Erro Seguro: Qualquer falha na requisição é suprimida, garantindo que o comportamento do componente não seja afetado por erros momentâneos de rede.

<app.Link href=`/about` prefetch>
  Sobre Nós
</app.Link>
Enter fullscreen mode Exit fullscreen mode

Quando o usuário passar o mouse sobre o link “Sobre Nós”, o componente já começará a pré-carregar os dados da página /about, proporcionando uma transição quase instantânea. Idéia genial, não? Mas vi na documentação do react.dev.

app.useContext: O Portal para o Edge

O app.useContext é o hook fundamental do React Edge, proporcionando acesso a todo contexto do worker:

const DashboardPage = () => {
 const {
   // Parâmetros da rota atual
   pathParams,
   // Query params (já parseados)
   searchParams,
   // Rota que deu match
   path,
   // Rota original (com parâmetros)
   rawPath,
   // Proxy para RPC
   rpc,
   // Store compartilhado
   store,
   // URL completa
   url
 } = app.useContext<App.Context>();

 // Tipagem completa do RPC
 const { data } = app.useFetch(
   async (ctx) => ctx.rpc.getDashboardStats()
 );

 // Acesso aos dados do store
 const user = store.get('user');

 return (
   <div>
     <h1>Dashboard para {user.name}</h1>
     <p>Visualizando: {path}</p>
   </div>
 );
};
Enter fullscreen mode Exit fullscreen mode

Principais Recursos do app.useContext

  • Gerenciamento de Rotas: Obtenha acesso à rota correspondente, seus parâmetros e strings de consulta sem esforço.
  • Integração RPC: Faça chamadas RPC tipadas e seguras diretamente do cliente sem configuração adicional.
  • Acesso à Store Compartilhada: Recupere ou defina valores no estado compartilhado worker-cliente com controle total sobre a visibilidade (público/privado).
  • Acesso Universal à URL: Acesse facilmente a URL completa da requisição atual para renderização e interações dinâmicas.

Por Que É Poderoso

O hook app.useContext preenche a lacuna entre o worker e o cliente. Ele permite que você construa recursos que dependem de estado compartilhado, busca segura de dados e renderização contextual sem código repetitivo. Isso simplifica aplicações complexas, tornando-as mais fáceis de manter e mais rápidas de desenvolver.

app.useUrlState: Estado Sincronizado com a URL

O hook app.useUrlState mantém o estado da sua aplicação sincronizado com os parâmetros da URL, oferecendo controle preciso sobre o que é incluído na URL, como o estado é serializado e quando é atualizado.

const ProductsPage = () => {
  // Estado automaticamente sincronizado com query params
  const [filters, setFilters] = app.useUrlState({
      categoria: 'todos',
      precoMinimo: 0,
      precoMaximo: 1000,
      outros: {
          foo: 'bar',
          baz: 'qux'
      }
  }, {
      debounce: 500,              // Atrasa atualizações da URL em 500ms
      kebabCase: true,            // Converte chaves para kebab-case para URLs mais limpas
      omitKeys: ['filtro.locais'], // Exclui chaves específicas da URL
      omitValues: [],             // Exclui valores específicos da URL
      pickKeys: [],               // Inclui apenas chaves específicas na URL
      prefix: '',                 // Adiciona um prefixo opcional às chaves de consulta
      url: ctx.url                // Usa a URL atual do contexto (funciona no servidor)
  });

  const { data } = app.useFetch(
    async (ctx) => ctx.rpc.produtos.buscar(filters),
    {
      // Refetch quando filters mudar
      deps: [filters]
    }
  );

  return (
    <div>
      <PainelFiltros
        value={filters}
        onChange={(novosFiltros) => {
          // URL é atualizada automaticamente
          setFilters(novosFiltros);
        }}
      />
      <GradeProdutos data={data} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Parâmetros

  1. Estado Inicial

    • Um objeto definindo a estrutura padrão e valores para seu estado.
  2. Opções:

    • debounce: Controla a rapidez com que a URL é atualizada após mudanças de estado.
    • kebabCase: Converte chaves de estado para kebab-case ao serializar para a URL.
    • omitKeys: Especifica chaves a serem excluídas da URL.
    • omitValues: Valores que, quando presentes, excluirão a chave associada da URL.
    • pickKeys: Limita o estado serializado para incluir apenas chaves específicas.
    • prefix: Adiciona um prefixo a todos os parâmetros de consulta.
    • url: A URL base para sincronização, geralmente derivada do contexto da aplicação.

Benefícios

  • Amigável para SEO: Garante que visualizações dependentes de estado sejam refletidas em URLs compartilháveis.
  • Atualizações com Debounce: Previne atualizações excessivas de consultas para entradas que mudam rapidamente.
  • URLs Limpas: Opções como kebabCase e omitKeys mantêm as strings de consulta legíveis.
  • Hidratação de Estado: Inicializa automaticamente o estado da URL na montagem do componente.
  • Funciona em todos ambientes: Suporta renderização no servidor e navegação no cliente.

Aplicações Práticas

  • Filtros para Listagens: Sincroniza filtros aplicados pelo usuário com a URL.
  • Visualizações Dinâmicas: Garante que zoom do mapa, pontos centrais ou outras configurações persistam.
  • Preferências do Usuário: Salva configurações selecionadas na URL para compartilhamento.

app.useStorageState: Estado Persistente

O hook app.useStorageState permite persistir estado no navegador usando localStorage ou sessionStorage com suporte completo a tipagem.

type RecentSearch = {
  term: string;
  date: string;
  category: string;
}

type SearchState {
  recentSearches: RecentSearch[];
  favorites: string[];
  lastSearch?: string;
}

const RecentSearches = () => {
  // Inicializa o estado com tipagem e valores padrão
  const [searchState, setSearchState] = app.useStorageState<SearchState>(
    // Chave única para armazenamento
    'user-searches',
    // Estado inicial
    {
      recentSearches: [],
      favorites: [],
      lastSearch: undefined
    },
    // Opções de configuração
    {
      // Atrasa a gravação no storage por 500ms para otimizar performance
      debounce: 500,

      // Define o tipo de armazenamento:
      // 'local' - persiste mesmo após fechar o navegador
      // 'session' - persiste apenas durante a sessão
      storage: 'local',

      // Ignora estas chaves ao salvar no storage
      omitKeys: ['lastSearch'],

      // Salva apenas estas chaves específicas
      pickKeys: ['recentSearches', 'favorites']
    }
  );

  return (
    <div className="recent-searches">
      <h3>Buscas Recentes</h3>

      {/* Lista as buscas recentes */}
      <ul>
        {searchState.recentSearches.map((search, index) => (
          <li key={index}>
            <span>{search.term}</span>
            <small>{search.date}</small>

            {/* Botão para favoritar busca */}
            <button
              onClick={() => {
                setSearchState({
                  ...searchState,
                  favorites: [...searchState.favorites, search.term]
                })
              }}
            ></button>
          </li>
        ))}
      </ul>

      {/* Botão para limpar histórico */}
      <button
        onClick={() => {
          setSearchState({
            ...searchState,
            recentSearches: []
          })
        }}
      >
        Limpar Histórico
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Opções de Persistência

  • debounce: Controla frequência de gravação
  • storage: Escolha entre localStorage e sessionStorage
  • omitKeys/pickKeys: Controle fino sobre dados persistidos

Performance

  • Atualizações otimizadas com debounce
  • Serialização/deserialização automática
  • Cache em memória

Casos de Uso Comuns

  • Histórico de pesquisas
  • Lista de favoritos
  • Preferências do usuário
  • Estado de filtros
  • Carrinho de compras temporário
  • Rascunhos de formulários

app.useDebounce: Controle de Frequência

Debounce valores reativos com facilidade:

const SearchInput = () => {
  const [input, setInput] = useState('');

  // Valor debounced atualiza apenas após 300ms de 'silêncio'
  const debouncedValue = app.useDebounce(input, 300);

  const { data } = app.useFetch(
    async (ctx) => ctx.rpc.search(debouncedValue),
    {
      // Fetch acontece apenas quando o valor debounced muda
      deps: [debouncedValue]
    }
  );

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder='Buscar...'
      />
      <SearchResults data={data} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

app.useDistinct: Estado sem Duplicatas

Mantenha arrays de valores únicos com tipagem:

O app.useDistinct é um hook especializado em detectar quando um valor realmente mudou, com suporte a comparação profunda e debounce:

const SearchResults = () => {
  const [search, setSearch] = useState('');

  // Detecta mudanças distintas no valor de busca
  const {
    value: currentSearch,   // Valor atual
    prevValue: lastSearch,  // Valor anterior
    distinct: hasChanged    // Indica se houve mudança
  } = app.useDistinct(search, {
    // Debounce de 300ms
    debounce: 300,
    // Comparação profunda
    deep: true,
    // Função de comparação customizada
    compare: (a, b) => a?.toLowerCase() === b?.toLowerCase()
  });

  // Fetch apenas quando a busca realmente mudar
  const { data } = app.useFetch(
    async (ctx) => ctx.rpc.search(currentSearch),
    {
      deps: [currentSearch],
      // Só executa se houve mudança distinta
      shouldFetch: () => hasChanged
    }
  );

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />

      {hasChanged && (
        <small>
          Busca alterada de '{lastSearch}' para '{currentSearch}'
        </small>
      )}

      <SearchResults data={data} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Principais Recursos

  1. Detecção de Valor Distinto:
    • Monitora os valores atual e anterior
    • Detecta automaticamente se uma mudança é significativa com base em seus critérios
  2. Comparação Profunda:
    • Permite verificações de igualdade em nível profundo para objetos complexos
  3. Comparação Personalizada:
    • Suporta funções personalizadas para definir o que constitui uma mudança "distinta"
  4. Debounced:
    • Reduz atualizações desnecessárias quando as mudanças ocorrem com muita frequência

Benefícios

  • API idêntica ao useState: Fácil de integrar em componentes existentes.
  • Desempenho Otimizado: Evita refetching ou recálculos desnecessários quando o valor não mudou significativamente. UX Aprimorada: Previne atualizações excessivamente reativas da UI, resultando em interações mais suaves.
  • Lógica Simplificada: Elimina verificações manuais de igualdade ou duplicação no gerenciamento de estado.

Os hooks do React Edge foram desenhados para trabalhar em harmonia, proporcionando uma experiência de desenvolvimento fluida e tipada. A combinação deles permite criar interfaces complexas e reativas com muito menos código.

A CLI do React Edge: Potência na Ponta dos Dedos

A CLI do React Edge foi projetada para simplificar a vida dos desenvolvedores, reunindo ferramentas essenciais em uma interface única e intuitiva. Seja você iniciante ou experiente, a CLI garante que você possa configurar, desenvolver, testar e implantar projetos com eficiência e sem complicações.

Principais Recursos

Comandos Modulares e Flexíveis:

  • build: Constrói tanto o app quanto o worker, com opções para especificar ambientes e modos de desenvolvimento ou produção.
  • dev: Inicia servidores de desenvolvimento locais ou remotos, permitindo trabalhar separadamente no app ou no worker.
  • deploy: Realiza deploys rápidos e eficientes utilizando o poder combinado do Cloudflare Workers e do Cloudflare R2, garantindo performance e escalabilidade na infraestrutura edge.
  • logs: Monitora logs do worker diretamente no terminal.
  • lint: Automatiza a execução do Prettier e do ESLint, com suporte a correções automáticas.
  • test: Executa testes com cobertura opcional usando Vitest.
  • type-check: Valida a tipagem TypeScript no projeto.

Casos de Uso em Produção

Tenho o orgulho de compartilhar que a primeira aplicação em produção utilizando o React Edge já está funcionando! Trata-se de uma imobiliária brasileira, a Lopes Imóveis, que já está aproveitando toda a performance e flexibilidade do framework.

No site da imobiliária, os imóveis são carregados em cache para otimizar a busca e oferecer uma experiência mais fluida para os usuários. Por se tratar de um site extremamente dinâmico, o cache das rotas utiliza um TTL de apenas 10 segundos, combinado com a estratégia stale-while-revalidate. Isso garante que o site entregue dados atualizados com uma performance excepcional, mesmo durante revalidações em segundo plano.

Além disso, as recomendações de imóveis similares são calculadas de maneira eficiente e eventual em background, e salvas diretamente no cache da Cloudflare, utilizando o sistema de cache integrado ao RPC. Essa abordagem reduz o tempo de resposta em requisições subsequentes e torna a query das recomendações quase instantânea. Além de que todas as imagens estão armazenadas no Cloudflare R2, oferecendo armazenamento escalável e distribuído sem depender de provedores externos.

app em producao usando react edge
app em producao usando react edge

E logo teremos também o lançamento de um projeto gigantesco de marketing automatizado para a Easy Auth, mostrando ainda mais o potencial dessa tecnologia.

Conclusão

E assim, caros leitores, chegamos ao fim desta aventura pelo universo do React Edge! Sei que ainda há um mar de coisas incríveis para explorar, como as autenticações mais simples como Basic e Bearer, e outros segredinhos que fazem o dia a dia de um dev muito mais feliz. Mas calma lá! A ideia é trazer mais artigos detalhados no futuro para mergulhar de cabeça em cada uma dessas funcionalidades.

E, spoiler: logo o React Edge será open source e devidamente documentado! Conciliar desenvolvimento, trabalho, escrever e um pouquinho de vida social não é fácil, mas a empolgação de ver essa maravilha em ação, especialmente com a velocidade absurda proporcionada pela infraestrutura da Cloudflare, é o combustível que me move. Então, segura a ansiedade, porque o melhor ainda está por vir! 🚀

Enquanto isso, se você quiser começar a explorar e testar agora mesmo, o pacote já está disponível no NPM: React Edge no NPM..

Meu e-mail é feliperohdee@gmail.com, e estou sempre aberto a feedbacks, esta é só inicio desta jornada, sugestões e críticas construtivas. Se você gostou do que leu, compartilhe com seus amigos e colegas, e fique de olho nas novidades que estão por vir. Obrigado por me acompanhar até aqui, e até a próxima! 🚀🚀🚀

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (3)

Collapse
 
vinirssantos profile image
Vinicius dos Santos

Excelente artigo, Felipe! 🎉
Parabéns pelo React Edge. Até que enfim temos uma solução que consegue resolver, de forma simples, essas lacunas dos frameworks atuais.👏
É incrivel o potencial e a agilidade que isso proporciona para nós desenvolvedores.

Collapse
 
carol_lopes_5a735bb695192 profile image
Carol Lopes

🚀🚀👏👏

Collapse
 
gilneidp profile image
Gilnei de Pellegrin • Edited

Simplesmente incrível o que saiu dessa mente. E o mais legal de tudo, ver soluções sendo criadas em uma velocidade absurda. Parabéns, Rohde!

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series