- A Gota D'água
- A Alternativa com Cloudflare 😍
- React Edge: O Framework React derivado de todas (ou quase) as dores de um desenvolvedor
- Além do useFetch: O arsenal completo
- Link: O Componente que Pensa à Frente
- app.useContext: O Portal para o Edge
- app.useUrlState: Estado Sincronizado com a URL
- app.useStorageState: Estado Persistente
- app.useDebounce: Controle de Frequência
- app.useDistinct: Estado sem Duplicatas
- A CLI do React Edge: Potência na Ponta dos Dedos
- Conclusã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' })
);
};
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>();
// ✅ 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>();
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>();
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>;
};
Por Que Isso é Revolucionário?
-
Zero Boilerplate
- Sem gerenciamento manual de cookies
- Sem necessidade de interceptors
- Sem refresh tokens manual
-
Segurança por Padrão
- Tokens são automaticamente encriptados
- Cookies são seguros e httpOnly
- Revalidação automática
-
Tipagem Completa
- Payload do JWT é tipado
- Validação com Zod integrada
- Erros de autenticação tipados
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>
);
};
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
- Segurança: A separação dos escopos de dados públicos e 2. privados garante que as informações sensíveis permaneçam protegidas.
- Conveniência: O acesso transparente aos dados da store simplifica o gerenciamento de estado entre worker e cliente.
- 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 }
};
}
}
})
]
})
]
};
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
- Consistência: Ao agrupar rotas relacionadas, você garante a aplicação consistente de middleware e organização do código.
- Escalabilidade: O sistema suporta roteamento aninhado e modular para aplicações em grande escala.
- 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']
});
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
- Desempenho Aprimorado: Reduz a carga nas APIs ao fornecer respostas em cache para dados frequentemente acessados.
- Escalabilidade: Gerencia eficientemente grandes conjuntos de dados e alto tráfego com um sistema de cache distribuído.
- 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?
Prefetch Condicional: O atributo prefetch (ativo por padrão) controla se o pré-carregamento será realizado.
Cache Inteligente: Um conjunto (Set) é usado para armazenar os links já pré-carregados, evitando chamadas redundantes.
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.
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>();
// 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>
);
};
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>
);
};
Parâmetros
-
Estado Inicial
- Um objeto definindo a estrutura padrão e valores para seu estado.
-
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>
);
};
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>
);
};
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>
);
};
Principais Recursos
- Detecção de Valor Distinto:
- Monitora os valores atual e anterior
- Detecta automaticamente se uma mudança é significativa com base em seus critérios
- Comparação Profunda:
- Permite verificações de igualdade em nível profundo para objetos complexos
- Comparação Personalizada:
- Suporta funções personalizadas para definir o que constitui uma mudança "distinta"
- 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.
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)
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.
🚀🚀👏👏
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!