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

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! 🚀🚀🚀

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!