Skip to content

Instantly share code, notes, and snippets.

@feliperohdee
Created November 18, 2024 16:05
Show Gist options
  • Save feliperohdee/d1594eab6377de094abbd2a9d2d3d6ee to your computer and use it in GitHub Desktop.
Save feliperohdee/d1594eab6377de094abbd2a9d2d3d6ee to your computer and use it in GitHub Desktop.

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

Índice Rápido

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.

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<UserAPI>>();
  
  // 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' })
  );
};

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
}

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>
 );
};

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!
};

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>
  );
};

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<PaymentsAPI>>();

 // ✅ 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;
};

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<UsersAPI>>();
  
  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');
};

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}!',
};

Uso no código:

const WelcomeMessage = () => {
  const userName = 'João';
  
  return (
    <div>
      {/* Output: Bem vindo, João! */}
      <h1>{__('Good Morning, {name}!', { name: userName })}</h1>
  );
};

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;
	}
};

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 });
 }
}

Uso no Cliente: Zero Configuração

const LoginForm = () => {
  const { rpc } = app.useContext<App.Context<SessionAPI>>();
  
  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<SessionAPI>>();
  
  const logout = async () => {
    await rpc.signout();
    // Cookies já foram limpos automaticamente
  };

  return <button onClick={logout}>Sair</button>;
};

Por Que Isso é Revolucionário?

  1. Zero Boilerplate
  • Sem gerenciamento manual de cookies
  • Sem necessidade de interceptors
  • Sem refresh tokens manual
  1. Segurança por Padrão
  • Tokens são automaticamente encriptados
  • Cookies são seguros e httpOnly
  • Revalidação automática
  1. Tipagem Completa
  • Payload do JWT é tipado
  • Validação com Zod integrada
  • Erros de autenticação tipados
  1. 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: [/*...*/]
    })
  ]
};

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>
  );
};

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 }
              };
            }
          }
        })
      ]
    })
  ]
};

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']
});

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>

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<DashboardAPI>>();

 // 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>
 );
};

app.useUrlState: Estado Sincronizado com a URL

Mantenha o estado do seu app sincronizado com a URL de forma elegante:

const ProductsPage = () => {
  // Estado automaticamente sincronizado com query params
  const [filters, setFilters] = app.useUrlState({
    // Chave na URL
    key: 'filters',
    // Valor inicial
    defaultValue: {
      category: 'all',
      minPrice: 0,
      maxPrice: 1000
    },
    // Validação opcional com Zod
    schema: filtersSchema
  });

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

  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={(newFilters) => {
          // URL é atualizada automaticamente
          setFilters(newFilters);
        }}
      />
      <ProductGrid data={data} />
    </div>
  );
};

app.useStorageState: Estado Persistente

Persista estado no localStorage/sessionStorage com tipagem:

const ThemeToggle = () => {
  // Estado persistido no localStorage
  const [theme, setTheme] = app.useStorageState({
    key: 'theme',
    defaultValue: 'light',
    // localStorage ou sessionStorage
    storage: 'local',
    // Validação opcional
    validate: (value) => ['light', 'dark'].includes(value)
  });

  // Versão com sessionStorage
  const [recentSearches, setRecentSearches] = app.useStorageState({
    key: 'searches',
    defaultValue: [],
    storage: 'session'
  });

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle {theme} mode
    </button>
  );
};

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>
  );
};

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>
  );
};

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.

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 é [email protected], 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! 🚀🚀🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment