Home >Web Front-end >JS Tutorial >From Next.js to React Edge with Cloudflare Workers: A Liberation Story

From Next.js to React Edge with Cloudflare Workers: A Liberation Story

DDD
DDDOriginal
2024-11-20 13:58:12924browse
  • The Last Straw
  • The Alternative with Cloudflare?
  • React Edge: The React Framework derived from all (or almost) a developer's pains
    • The Magic of Typed RPC
    • The Power of useFetch: Where the Magic Happens
  • Beyond useFetch: The complete arsenal
    • RPC: The Art of Client-Server Communication
    • An i18n System that Makes Sense
    • JWT authentication that “Just Works”
    • The Shared Store
    • Elegant Routing
    • Distributed Cache with Edge Cache
  • Link: The Forward Thinking Component
  • app.useContext: The Gateway to Edge
  • app.useUrlState: Status Synchronized with URL
  • app.useStorageState: Persistent State
  • app.useDebounce: Frequency Control
  • app.useDistinct: Status without Duplicates
  • The React Edge CLI: Power at Your Fingertips
  • Conclusion

The Last Drop

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.

The Alternative with Cloudflare?

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.

React Edge: The React Framework derived from all (or almost) a developer's pains

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.

Rethinking React Development for the Edge Computing Era

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?

With React Edge, you don't need to:

  • Create separate API routes
  • Manage loading/error status manually
  • Implement debounce by hand
  • Worry about serialization/deserialization
  • Handle CORS
  • Manage typing between client/server
  • Handle authentication rules manually
  • Manage how internationalization is done

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?

The Magic of Typed RPC

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

Compare this to 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
}

The Power of useFetch: Where the Magic Happens

Data Fetching Rethought

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:

  • Preload data to server during SSR
  • Automatically moisturizes on the client without flicker
  • Maintains complete typing between client and server
  • Supports reactivity with smart debounce
  • Automatically multiplexes identical calls
  • Allows programmatic updates and polling

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 Magic of Multiplexing

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
}

SSR Perfect Hydration

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

Beyond useFetch: The complete arsenal

RPC: The Art of Client-Server Communication

Security and Encapsulation

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

RPC API Hierarchy

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

Benefits of Hierarchy

Organizing APIs into hierarchies brings several benefits:

  • Logical Organization: Group related functionalities in an intuitive way
  • Natural Namespace: Avoid name conflicts with clear paths (users.preferences.getTheme)
  • Encapsulation: Keep helper methods private at each level
  • Maintainability: Each subclass can be maintained and tested independently
  • Complete Typing: TypeScript understands the entire hierarchy

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.

An i18n System that Makes Sense

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

Zero Configuration

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

JWT Authentication that “Just Works”

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
}

Client Use: Zero Configuration

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

Why is this revolutionary?

  1. Zero Boilerplate

    • No manual cookie management
    • No need for interceptors
    • No upgrade token manual
  2. Security by Default

    • Tokens are automatically encrypted
    • Cookies are safe and httpOnly
    • Automatic revalidation
  3. Complete Typing

    • JWT payload is entered
    • Validation with Zod integrated
    • Typed authentication errors
  4. 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!
};

The Shared Store

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

How It Works

  • Public Data: Data marked as public is shared securely with the client, making it easily accessible to components.
  • Private Data: Sensitive data remains within the worker environment and is never exposed to the client.
  • Middleware Integration: Middleware can populate the store with public and private data, ensuring a continuous flow of information between server logic and client-side rendering.

Benefits

  1. Security: Separation of public and private data scopes ensures that confidential information remains protected.
  2. Convenience: Transparent access to store data simplifies state management between worker and client.
  3. Flexibility: The store is easily integrated with middleware, allowing dynamic status updates based on request handling.

Elegant Routing

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

Key Features

  • Grouped Routes: Logical grouping of related routes under a shared path and middleware. Flexible Handlers: Define handlers that return pages or direct API responses.
  • Per-Route Headers: Customize HTTP headers for individual routes.
  • Integrated Cache: Simplify caching strategies with ttl and tags.

Benefits

  1. Consistency: By grouping related routes together, you ensure consistent middleware application and code organization.
  2. Scalability: The system supports nested and modular routing for large-scale applications.
  3. Performance: Native cache support ensures optimal response times without manual configurations.

Distributed Cache with Edge Cache

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
}

Key Features

  • Tag-Based Invalidation: Cache entries can be grouped using tags, allowing for easy and selective invalidation when data changes.
  • Prefix Match: Invalidate multiple cache entries using a common prefix, ideal for scenarios such as search queries or hierarchical data.
  • Time to Live (TTL): Set expiration times for cache entries to ensure fresh data while maintaining high performance.

Benefits

  1. Improved Performance: Reduces load on APIs by providing cached responses for frequently accessed data.
  2. Scalability: Efficiently manages large data sets and high traffic with a distributed caching system.
  3. Flexibility: Fine-grained control over caching, allowing developers to optimize performance without sacrificing data accuracy.

Link: The Forward Thinking Component

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?

  1. Conditional Prefetch: The prefetch attribute (active by default) controls whether preloading will be performed.

  2. Smart Cache: A Set is used to store the already preloaded links, avoiding redundant calls.

  3. 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.

  4. 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: The Gateway to Edge

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
}

Main Features of app.useContext

  • Route Management: Get access to the corresponding route, its parameters and query strings effortlessly.
  • RPC Integration: Make typed and secure RPC calls directly from the client without additional configuration.
  • Shared Store Access: Retrieve or set values ​​in the worker-client shared state with full control over visibility (public/private).
  • Universal URL Access: Easily access the full URL of the current request for dynamic rendering and interactions.

Why It's Powerful

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.

app.useUrlState: State Synchronized with the URL

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

Parameters

  1. Initial State

    • An object defining the default structure and values ​​for its state.
  2. Options:

    • debounce: Controls how quickly the URL is updated after state changes.
    • kebabCase: Converts state keys to kebab-case when serializing to URL.
    • omitKeys: Specifies keys to be excluded from the URL.
    • omitValues: Values ​​that, when present, will exclude the associated key from the URL.
    • pickKeys: Limits the serialized state to include only specific keys.
    • prefix: Adds a prefix to all query parameters.
    • url: The base URL for synchronization, usually derived from the application context.

Benefits

  • SEO Friendly: Ensures state-dependent views are reflected in shareable URLs.
  • Updates with Debounce: Prevents excessive query updates for rapidly changing inputs.
  • Clean URLs: Options like kebabCase and omitKeys keep query strings readable.
  • State Hydration: Automatically initializes the state of the URL when assembling the component.
  • Works in all environments: Supports server-side rendering and client-side browsing.

Practical Applications

  • Filters for Listings: Synchronizes filters applied by the user with the URL.
  • Dynamic Views: Ensures map zoom, center points or other settings persist.
  • User Preferences: Saves selected settings to URL for sharing.

app.useStorageState: Persistent State

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

Persistence Options

  • debounce: Controls recording frequency
  • storage: Choose between localStorage and sessionStorage
  • omitKeys/pickKeys: Fine control over persisted data

Performance

  • Updates optimized with debounce
  • Automatic serialization/deserialization
  • Cache in memory

Common Use Cases

  • Search history
  • Favorites list
  • User Preferences
  • Filter status
  • Temporary shopping cart
  • Draft forms

app.useDebounce: Frequency Control

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
}

app.useDistinct: State without Duplicates

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

Key Features

  1. Distinct Value Detection:
    • Monitors current and previous values
    • Automatically detects whether a change is significant based on your criteria
  2. Deep Comparison:
    • Enables deep-level equality checks for complex objects
  3. Personalized Comparison:
    • Supports custom functions to define what constitutes a "distinct" change
  4. Debounced:
    • Reduces unnecessary updates when changes occur too frequently

Benefits

  • API identical to useState: Easy to integrate into existing components.
  • Optimized Performance: Prevents unnecessary refetching or recalculations when the value has not changed significantly. Improved UX: Prevents overly reactive UI updates, resulting in smoother interactions.
  • Simplified Logic: Eliminates manual checks for equality or duplication in state management.

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: Power at Your Fingertips

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

Modular and Flexible Commands:

  • build: Builds both the app and the worker, with options to specify development or production environments and modes.
  • dev: Launches local or remote development servers, allowing you to work separately on the app or worker.
  • deploy: Performs fast and efficient deployments using the combined power of Cloudflare Workers and Cloudflare R2, ensuring performance and scalability on the edge infrastructure.
  • logs: Monitor worker logs directly in the terminal.
  • lint: Automates the execution of Prettier and ESLint, with support for automatic corrections.
  • test: Runs tests with optional coverage using Vitest.
  • type-check: Validates the TypeScript typing in the project.

Production Use Cases

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.

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

And soon we will also have the launch of a gigantic automated marketing project for Easy Auth, further demonstrating the potential of this technology.

Conclusion

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!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn