- De Next.js a React Edge com Cloudflare Workers: Uma História de Libertação
- 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
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.
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.
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.
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?
- 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á?
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' })
);
};
// 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
}
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>
);
};
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!
};
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>
);
};
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;
};
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');
};
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.
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>
);
};
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;
}
};
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 });
}
}
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>;
};
- 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: [/*...*/]
})
]
};
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>
);
};
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 }
};
}
}
})
]
})
]
};
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']
});
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.
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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 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
- 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.
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! 🚀🚀🚀