It all started with an invoice from Vercel. No, it actually started much earlier - with small frustrations that built up. The need to pay for basic features like DDoS protection, more detailed logs, or even a decent firewall, build queues, etc. The feeling of being trapped in an increasingly expensive vendor lock-in.
"And worst of all: our precious SEO headers simply stopped being rendered on the server in an application using the pages router. A real headache for any dev! ?"
But what really made me rethink everything was the direction Next.js was taking. The introduction of usage client, usage server - directives that in theory should simplify development, but in practice added another layer of complexity to manage. It was as if we were going back to the days of PHP, marking files with directives to tell them where they should run.
And it doesn't stop there. The App Router, an interesting idea, but implemented in a way that created a practically new framework within Next.js. Suddenly we had two completely different ways of doing the same thing. The 'old' and the 'new' - with subtly different behaviors and hidden traps.
That's when I realized: why not take advantage of Cloudflare's incredible infrastructure with Workers running at the edge, R2 for storage, KV for distributed data... Plus, of course, the incredible DDoS protection, global CDN, firewall, rules for pages and routes and everything else Cloudflare offers.
And the best: a fair price model, where you pay for what you use, without surprises.
This is how React Edge was born. A framework that doesn't try to reinvent the wheel, but rather provides a truly simple and modern development experience.
When I started developing React Edge, I had a clear goal: to create a framework that made sense. No more struggling with confusing directives, no more paying fortunes for basic features, and most importantly, no more having to deal with the artificial complexity created by the client/server separation. I wanted speed, something that delivered performance without sacrificing simplicity. Leveraging my knowledge of the React API and years as a Javascript and Golang developer, I knew exactly how to handle streams and multiplexing to optimize rendering and data management.
Cloudflare Workers, with its powerful infrastructure and global presence, offered me the perfect environment to explore these possibilities. I wanted something that was truly hybrid, and this combination of tools and experience was what gave life to React Edge: a framework that solves real problems with modern and efficient solutions.
React Edge brings a revolutionary approach to React development. Imagine being able to write a class on the server and call it directly from the client, with full typing and zero configuration. Imagine a distributed caching system that "just works", allowing invalidation by tags or prefixes. Imagine being able to share state between server and client in a transparent and secure way. In addition to simplifying authentication and bringing an efficient internationalization approach, CLI and much more.
Your RPC communication is so natural it seems like magic - you write methods in a class and call them from the client as if they were local. The intelligent multiplexing system ensures that, even if multiple components make the same call, only one request is made to the server. The ephemeral cache avoids unnecessary repeated requests, and all of this works on both the server and the client.
One of the most powerful points is the app.useFetch hook, which unifies the data fetching experience. On the server, it preloads data during SSR; on the client, it automatically hydrates with this data and allows for on-demand updates. And with support for automatic polling and dependency-based reactivity, creating dynamic interfaces has never been easier.
But it doesn't stop there. The framework offers a powerful routing system (inspired by the fantastic Hono), asset management integrated with Cloudflare R2, and an elegant way to handle errors through the HttpError class. Middleware can easily send data to the client through a shared store, and everything is automatically obfuscated for security.
The most impressive? Almost all of the framework's code is hybrid. There is no 'client' and a 'server' version - the same code works in both environments, automatically adapting to the context. The customer only receives what they need, making the final bundle extremely optimized.
And the icing on the cake: all of this runs on the Cloudflare Workers edge infrastructure, providing exceptional performance at a fair cost. No surprises on the bill, no basic features hidden behind forced enterprise plans, just a solid framework that allows you to focus on what really matters: creating incredible applications. Additionally, React Edge leverages the entire Cloudflare ecosystem, including Queues, Durable Objects, KV Storage, and more to provide a robust, scalable foundation for your applications.
Vite was used as a base, both for the development environment and for testing and build. Vite, with its impressive speed and modern architecture, enables an agile and efficient workflow. It not only speeds up development, but also optimizes the build process, ensuring that code is compiled quickly and accurately. Without a doubt, Vite was the perfect choice for React Edge.
Have you ever wondered what it would be like to develop React applications without worrying about the client/server barrier? Without having to memorize dozens of directives like use client or use server? And even better: what if you could call server functions as if they were local, with full typing and zero configuration?
And the best part: all of this works on both the server and the client, without having to mark anything with use client or use server. The framework knows what to do based on the context. Shall we go?
Imagine being able to do this:
// 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' }) ); };
// 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 }
Forget everything you know about data fetching in React. React Edge's app.useFetch brings a completely new and powerful approach. Imagine a hook that:
Let's see this in action:
// 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' }) ); };
The example above hides a powerful feature: intelligent multiplexing. When you use ctx.rpc.batch, React Edge doesn't just batch the calls - it automatically deduplicates identical calls:
// 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 }
One of the most impressive parts is how useFetch handles SSR:
// 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> ); };
React Edge's RPC system was designed with security and encapsulation in mind. Not everything in an RPC class is automatically exposed to the client:
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! };
One of the most powerful features of RPC is the ability to organize APIs into hierarchies:
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> ); };
Organizing APIs into hierarchies brings several benefits:
React Edge's RPC system makes client-server communication so natural that you almost forget you're making remote calls. And with the ability to organize APIs into hierarchies, you can create complex structures while keeping your code organized and secure.
React Edge brings an elegant and flexible internationalization system that supports variable interpolation and complex formatting without heavy libraries.
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; };
Usage in code:
// 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'); };
React Edge detects and loads your translations automatically, and can easily save user preferences in cookies. But you already expected that, right?
// 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' }) ); };
Authentication has always been a pain point in web applications. Managing JWT tokens, secure cookies, revalidation - all of this usually requires a lot of boilerplate code. React Edge changes this completely.
See how simple it is to implement a complete authentication system:
// 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 }
// 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> ); };
Zero Boilerplate
Security by Default
Complete Typing
Seamless Integration
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! };
One of React Edge's most powerful features is its ability to securely share state between worker and client. Let's see how this works:
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> ); };
React Edge's routing system is inspired by Hono, but with superpowers for SSR:
// 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' }) ); };
React Edge has a powerful caching system that works for both JSON data and entire pages:
// 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 }
The Link component is an intelligent and performant solution for preloading resources on the client side, ensuring more fluid and faster navigation for users. Its prefetching functionality is activated when hovering the cursor over the link, taking advantage of the user's moment of inactivity to request the destination data in advance.
How does it work?
Conditional Prefetch: The prefetch attribute (active by default) controls whether preloading will be performed.
Smart Cache: A Set is used to store the already preloaded links, avoiding redundant calls.
Mouse Enter: When the user hovers the cursor over the link, the handleMouseEnter function checks whether preloading is necessary and, if so, initiates a fetch request to the destination.
Error Safe: Any failure in the request is suppressed, ensuring that the component's behavior is not affected by momentary network errors.
// 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' }) ); };
When the user hovers the mouse over the “About Us” link, the component will begin preloading data from the /about page, providing an almost instantaneous transition. Brilliant idea, right? But I saw it in the react.dev documentation.
app.useContext is the fundamental hook of React Edge, providing access to the entire worker context:
// 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 }
The app.useContext hook bridges the gap between the worker and the client. It allows you to build features that rely on shared state, secure data fetching, and contextual rendering without repetitive code. This simplifies complex applications, making them easier to maintain and faster to develop.
The app.useUrlState hook keeps your application's state synchronized with URL parameters, giving you precise control over what is included in the URL, how the state is serialized, and when it is updated.
// 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> ); };
Initial State
Options:
The app.useStorageState hook allows you to persist state in the browser using localStorage or sessionStorage with full typing support.
// 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' }) ); };
Debounce reactive values with ease:
// 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 }
Maintain arrays of unique values with typing:
app.useDistinct is a hook specialized in detecting when a value has actually changed, with support for deep comparison and debounce:
// 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' }) ); };
React Edge hooks were designed to work in harmony, providing a fluid and typed development experience. Combining them allows you to create complex and reactive interfaces with much less code.
The React Edge CLI was designed to simplify developers' lives by bringing together essential tools in a single, intuitive interface. Whether you are a beginner or an expert, the CLI ensures that you can configure, develop, test and deploy projects efficiently and without hassle.
Key Features
I'm proud to share that the first production application using React Edge is now working! This is a Brazilian real estate company, Lopes Imóveis, which is already taking advantage of all the performance and flexibility of the framework.
On the real estate agency's website, properties are loaded in cache to optimize the search and offer a more fluid experience for users. As it is an extremely dynamic website, the route cache uses a TTL of just 10 seconds, combined with the stale-while-revalidate strategy. This ensures that the site delivers up-to-date data with exceptional performance, even during background revalidations.
In addition, recommendations for similar properties are calculated efficiently and occasionally in the background, and saved directly in Cloudflare's cache, using the cache system integrated into RPC. This approach reduces response time on subsequent requests and makes querying recommendations almost instantaneous. In addition, all images are stored on Cloudflare R2, offering scalable and distributed storage without relying on external providers.
And soon we will also have the launch of a gigantic automated marketing project for Easy Auth, further demonstrating the potential of this technology.
And so, dear readers, we come to the end of this adventure through the React Edge universe! I know there is still a sea of incredible things to explore, such as the simplest authentications like Basic and Bearer, and other little secrets that make a dev's daily life much happier. But calm down! The idea is to bring more detailed articles in the future to dive head first into each of these features.
And, spoiler: soon React Edge will be open source and properly documented! Balancing development, work, writing and a little social life is not easy, but the excitement of seeing this wonder in action, especially with the absurd speed provided by Cloudflare's infrastructure, is the fuel that moves me. So hold back your anxiety, because the best is yet to come! ?
In the meantime, if you want to start exploring and testing right now, the package is now available on NPM: React Edge on NPM..
My email is feliperohdee@gmail.com, and I am always open to feedback, this is just the beginning of this journey, suggestions and constructive criticism. If you liked what you read, share it with your friends and colleagues, and keep an eye out for new things to come. Thank you for following me this far, and until next time! ???
The above is the detailed content of From Next.js to React Edge with Cloudflare Workers: A Liberation Story. For more information, please follow other related articles on the PHP Chinese website!