Quick Index
- The Final Straw
- The Alternative with Cloudflare đ
- React Edge: The React Framework Born from Every Developerâs Pain (or Almost)
- Beyond useFetch: The Complete Arsenal
- Link: The Component That Thinks Ahead
- app.useContext: The Portal to the Edge
- app.useUrlState: State Synced with the URL
- app.useStorageState: Persistent State
- app.useDebounce: Frequency Control
- app.useDistinct: State Without Duplicates
- The React Edge CLI: Power at Your Fingertips
- Conclusion
The Final Straw
It all started with a Vercel invoice. No, actually, it started way earlierâwith small frustrations piling 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 rendering on the server in an application using the
pages router
. A true headache for any developer! đ"
But what truly made me rethink everything was the direction Next.js was heading. The introduction of use client
, use server
âdirectives that, in theory, should simplify development but, in practice, added another layer of complexity to manage. It was like going back to the PHP days, marking files with directives to dictate where they should run.
And it doesnât stop there. The App Routerâan interesting idea but implemented in a way that created an almost entirely new framework within Next.js. Suddenly, there were two completely different ways to do the same thingâthe 'old' and the 'new'âwith subtly different behaviors and hidden pitfalls.
The Alternative with Cloudflare đ
Thatâs when it hit me: why not leverage the incredible infrastructure of Cloudflare with Workers running on the edge, R2 for storage, KV for distributed data... Along with, of course, the amazing DDoS protection, global CDN, firewall, page rules and routing, and everything else Cloudflare has to offer.
And the best part: a fair pricing model where you pay for what you use, with no surprises.
This is how React Edge was born. A framework that doesnât aim to reinvent the wheel but instead delivers a truly simple and modern development experience.
React Edge: The React Framework Born from Every Developerâs Pain (or Almost)
When I started developing React Edge, I had a clear goal: to create a framework that made sense. No more wrestling with confusing directives, no more paying exorbitant fees for basic features, and most importantly, no more dealing with artificial complexity caused by client/server separation. I wanted speedâa framework that delivers performance without sacrificing simplicity. Leveraging my knowledge of Reactâs API and years of experience 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, provided the perfect environment to explore these possibilities. I wanted something truly hybrid, and this combination of tools and experience gave life to React Edge: a framework that solves real-world problems with modern and efficient solutions.
React Edge introduces a revolutionary approach to React development. Imagine writing a class on the server and calling it directly from the client, with full type safety and zero configuration. Imagine a distributed caching system that "just works," allowing invalidation by tags or prefixes. Imagine sharing state seamlessly and securely between server and client. Add simplified authentication, an efficient internationalization system, CLI, and more.
Its RPC communication feels almost magicalâ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 sent to the server. Ephemeral caching avoids unnecessary repeated requests, and it all works seamlessly on both the server and the client.
One of the most powerful features 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 those data and supports on-demand updates. With support for automatic polling and dependency-based reactivity, creating dynamic interfaces has never been easier.
But thatâs not all. The framework offers a powerful routing system (inspired by the fantastic Hono), integrated asset management with Cloudflare R2, and an elegant way to handle errors via the HttpError
class. Middlewares can easily send data to the client through a shared store, and everything is automatically obfuscated for security.
The most impressive part? Almost all of the frameworkâs code is hybrid. There isnât a âclientâ version and a âserverâ versionâthe same code works in both environments, adapting automatically to the context. The client receives only what it needs, making the final bundle extremely optimized.
And the icing on the cake: all of this runs on the Cloudflare Workers edge infrastructure, delivering exceptional performance at a fair cost. No surprise invoices, no basic features locked behind expensive enterprise plansâjust a solid framework that lets you focus on what truly matters: building amazing applications. Additionally, React Edge leverages Cloudflareâs ecosystem, including Queues, Durable Objects, KV Storage, and more, providing a robust and scalable foundation for your applications.
Vite was used as the base for the development environment, testing, and build process. With its impressive speed and modern architecture, Vite enables an agile and efficient workflow. It not only accelerates development but also optimizes the build process, ensuring fast and accurate compilation. 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 memorizing dozens of directives like use client
or use server
? Better yet: what if you could call server functions as if they were local, with full typing and zero configuration?
With React Edge, you no longer need to:
- Create separate API routes
- Manually manage loading/error state
- Implement debounce yourself
- Worry about serialization/deserialization
- Deal with CORS
- Manage typing between client/server
- Handle authentication rules manually
- Struggle with internationalization setup
And the best part: all this works seamlessly on both the server and client without marking anything as use client
or use server
. The framework automatically knows what to do based on the context. Shall we dive in?
The Magic of Typed RPC
Imagine being able to do this:
// On the server
class UserAPI extends Rpc {
async searchUsers(query: string, filters: UserFilters) {
// Validation with Zod
const validated = searchSchema.parse({ query, filters });
return this.db.users.search(validated);
}
}
// On the client
const UserSearch = () => {
const { rpc } = app.useContext<App.Context>();
// TypeScript knows exactly what searchUsers accepts and returns!
const { data, loading, error, fetch: retry } = app.useFetch(
async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
);
};
Compare this with Next.js/Vercel:
// pages/api/search.ts
export default async handler = (req, res) => {
// Configure CORS
// Validate request
// Handle errors
// Serialize response
// ...100 lines later...
}
// 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]);
// ... rest of the component
}
The Power of useFetch
: Where the Magic Happens
Rethinking Data Fetching
Forget everything you know about data fetching in React. The app.useFetch
hook from React Edge introduces a completely new and powerful approach. Imagine a hook that:
- Preloads data on the server during SSR.
- Automatically hydrates data on the client without flicker.
- Maintains full typing between client and server.
- Supports reactivity with intelligent debounce.
- Automatically multiplexes identical calls.
- Enables programmatic updates and polling.
Letâs see it in action:
// First, define your API on the server
class PropertiesAPI extends Rpc {
async searchProperties(filters: PropertyFilters) {
const results = await this.db.properties.search(filters);
// Automatic caching for 5 minutes
return this.createResponse(results, {
cache: { ttl: 300, tags: ['properties'] }
});
}
async getPropertyDetails(ids: string[]) {
return Promise.all(
ids.map(id => this.db.properties.findById(id))
);
}
}
// Now, on the client, the magic happens
const PropertySearch = () => {
const [filters, setFilters] = useState<PropertyFilters>({
price: { min: 100000, max: 500000 },
bedrooms: 2
});
// Reactive search with intelligent debounce
const {
data: searchResults,
loading: searchLoading,
error: searchError
} = app.useFetch(
async (ctx) => ctx.rpc.searchProperties(filters),
{
// Re-fetch when filters change
deps: [filters],
// Wait 300ms of "silence" before fetching
depsDebounce: {
filters: 300
}
}
);
// Fetch property details for the found results
const {
data: propertyDetails,
loading: detailsLoading,
fetch: refreshDetails
} = app.useFetch(
async (ctx) => {
if (!searchResults?.length) return null;
// This looks like multiple calls, but...
return ctx.rpc.batch([
// Everything is multiplexed into a single request!
...searchResults.map(result =>
ctx.rpc.getPropertyDetails(result.id)
)
]);
},
{
// Refresh when searchResults change
deps: [searchResults]
}
);
// A beautiful and responsive interface
return (
<div>
<FiltersPanel
value={filters}
onChange={setFilters}
disabled={searchLoading}
/>
{searchError && (
<Alert status='error'>
Search error: {searchError.message}
</Alert>
)}
<PropertyGrid
items={propertyDetails || []}
loading={detailsLoading}
onRefresh={() => refreshDetails()}
/>
</div>
);
};
The Magic of Multiplexing
The example above hides a powerful feature: intelligent multiplexing. When you use ctx.rpc.batch, React Edge not only groups callsâit also deduplicates identical calls automatically:
const PropertyListingPage = () => {
const { data } = app.useFetch(async (ctx) => {
// Even if you make 100 identical calls...
return ctx.rpc.batch([
ctx.rpc.getProperty('123'),
ctx.rpc.getProperty('123'), // same call
ctx.rpc.getProperty('456'),
ctx.rpc.getProperty('456'), // same call
]);
});
// Behind the scenes:
// 1. The batch groups all calls into ONE single HTTP request.
// 2. Identical calls are deduplicated automatically.
// 3. Results are distributed correctly to each position in the array.
// 4. Typing is maintained for each individual result!
// Actual RPC calls:
// 1. getProperty('123')
// 2. getProperty('456')
// Results are distributed correctly to all callers!
};
SSR + Perfect Hydration
One of the most impressive parts is how useFetch handles SSR:
const ProductPage = ({ productId }: Props) => {
const { data, loaded, loading, error } = app.useFetch(
async (ctx) => ctx.rpc.getProduct(productId),
{
// Fine-grained control over when to fetch
shouldFetch: ({ worker, loaded }) => {
// On the worker (SSR): always fetch
if (worker) return true;
// On the client: fetch only if no data is loaded
return !loaded;
}
}
);
// On the server:
// 1. `useFetch` makes the RPC call.
// 2. Data is serialized and sent to the client.
// 3. Component renders with the data.
// On the client:
// 1. Component hydrates with server data.
// 2. No new call is made (shouldFetch returns false).
// 3. If necessary, you can re-fetch with `data.fetch()`.
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductView
product={data}
loading={loading}
error={error}
/>
</Suspense>
);
};
Beyond useFetch: The Complete Arsenal
RPC: The Art of Client-Server Communication
Security and Encapsulation
The RPC system in React Edge is designed with security and encapsulation in mind. Not everything in an RPC class is automatically exposed to the client:
class PaymentsAPI extends Rpc {
// Properties are never exposed
private stripe = new Stripe(process.env.STRIPE_KEY);
// Methods starting with $ are private
private async $validateCard(card: CardInfo) {
return await this.stripe.cards.validate(card);
}
// Methods starting with _ are also private
private async _processPayment(amount: number) {
return await this.stripe.charges.create({ amount });
}
// This method is public and accessible via RPC
async createPayment(orderData: OrderData) {
// Internal validation using a private method
const validCard = await this.$validateCard(orderData.card);
if (!validCard) {
throw new HttpError(400, 'Invalid card');
}
// Processing using another private method
const payment = await this._processPayment(orderData.amount);
return payment;
}
}
// On the client:
const PaymentForm = () => {
const { rpc } = app.useContext<App.Context>();
// â
This works
const handleSubmit = () => rpc.createPayment(data);
// â These do not work - private methods are not exposed
const invalid1 = () => rpc.$validateCard(data);
const invalid2 = () => rpc._processPayment(100);
// â This also does not work - properties are not exposed
const invalid3 = () => rpc.stripe;
};
RPC API Hierarchies
One of the most powerful features of RPC is the ability to organize APIs into hierarchies:
// Nested APIs for better organization
class UsersAPI extends Rpc {
// Subclass to manage preferences
preferences = new UserPreferencesAPI();
// Subclass to manage notifications
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 {
// Private methods remain private
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);
}
}
}
// On the client:
const UserProfile = () => {
const { rpc } = app.useContext<App.Context>();
const { data: profile } = app.useFetch(
async (ctx) => {
// Nested calls are fully typed
const [user, theme, notificationSettings] = await ctx.rpc.batch([
// Method from the main class
ctx.rpc.getProfile('123'),
// Method from the preferences subclass
ctx.rpc.preferences.getTheme('123'),
// Method from the notifications subclass
ctx.rpc.notifications.getSettings('123')
]);
return { user, theme, notificationSettings };
}
);
// â Private methods remain inaccessible
const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};
Benefits of Hierarchies
Organizing APIs into hierarchies provides several benefits:
- Logical Organization: Group related functionalities intuitively.
- Natural Namespacing: Avoid name conflicts with clear paths (e.g., users.preferences.getTheme).
- Encapsulation: Keep helper methods private at each level.
- Maintainability: Each subclass can be maintained and tested independently.
- Full Typing: TypeScript understands the entire hierarchy.
The RPC system in React Edge makes client-server communication so natural that you almost forget youâre making remote calls. With the ability to organize APIs into hierarchies, you can build complex structures while keeping your code clean and secure.
A System of i18n That Makes Sense
React Edge introduces an elegant and flexible internationalization system that supports variable interpolation and complex formatting without relying on heavyweight libraries.
// translations/fr.ts
export default {
'Good Morning, {name}!': 'Bonjour, {name}!',
};
Usage in the code:
const WelcomeMessage = () => {
const userName = 'John';
return (
<div>
{/* Output: Good Morning, John! */}
<h1>{__('Good Morning, {name}!', { name: userName })}</h1>
</div>
);
};
Zero Configuration
React Edge automatically detects and loads your translations. It even allows saving user preferences in cookies effortlessly. But, of course, youâd expect this, right?
// 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 workerApp = 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;
}
};
JWT Authentication That "Just Works"
Authentication has always been a pain point in web applications. Managing JWT tokens, secure cookies, and revalidation often requires a lot of boilerplate code. React Edge completely changes this.
Hereâs how simple it is to implement a full authentication system:
class SessionAPI extends Rpc {
private auth = new AuthJwt({
// Cookie is automatically managed
cookie: 'token',
// Payload is automatically encrypted
encrypt: true,
// Automatic expiration
expires: { days: 1 },
secret: process.env.JWT_SECRET,
});
async signin(credentials: { email: string; password: string }) {
// Validation with Zod
const validated = loginSchema.parse(credentials);
// Signs the token and sets the headers automatically
const { headers } = await this.auth.sign(validated);
// Returns a response with configured cookies
return this.createResponse(
{ email: validated.email },
{ headers }
);
}
async getSession(revalidate = false) {
// Automatic token validation and revalidation
const { headers, payload } = await this.auth.authenticate(
this.request.headers,
revalidate
);
return this.createResponse(payload, { headers });
}
async signout() {
// Automatically clears cookies
const { headers } = await this.auth.destroy();
return this.createResponse(null, { headers });
}
}
Client Usage: Zero Configuration
const LoginForm = () => {
const { rpc } = app.useContext<App.Context>();
const login = async (values) => {
const session = await rpc.signin(values);
// Done! Cookies are automatically set
};
return <Form onSubmit={login}>...</Form>;
};
const NavBar = () => {
const { rpc } = app.useContext<App.Context>();
const logout = async () => {
await rpc.signout();
// Cookies are automatically cleared
};
return <button onClick={logout}>Log Out</button>;
};
Why Is This Revolutionary?
-
Zero Boilerplate
- No manual cookie management
- No need for interceptors
- No manual refresh tokens
-
Security by Default
- Tokens are automatically encrypted
- Cookies are secure and httpOnly
- Automatic revalidation
-
Full Typing
- JWT payload is typed
- Integrated Zod validation
- Typed authentication errors
Seamless Integration
// Middleware to protect routes
const authMiddleware: App.Middleware = async (ctx) => {
const session = await ctx.rpc.session.getSession();
if (!session) {
throw new HttpError(401, 'Unauthorized');
}
// Makes the session available to components
ctx.store.set('session', session, 'public');
};
// Usage in routes
const router: App.Router = {
routes: [
routerBuilder.routeGroup({
path: '/dashboard',
middlewares: [authMiddleware],
routes: [/*...*/],
}),
],
};
The Shared Store
One of the most powerful features of React Edge is its ability to securely share state between the worker and the client. Here's how it works:
Example of Middleware and Store Usage
// 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);
// Public data - automatically shared with the client
ctx.store.set(
'user',
{
id: user.id,
name: user.name,
role: user.role,
},
'public'
);
// Private data - remains accessible only within the worker
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>
);
};
How It Works
- Public Data: Data marked as public is securely shared with the client, making it easily accessible for components.
- Private Data: Sensitive data remains within the workerâs environment and is never exposed to the client.
- Integration with Middleware: Middleware can populate the store with both public and private data, ensuring a seamless flow of information between server-side logic and client-side rendering.
Benefits
- Security: Separate public and private data scopes ensure sensitive information stays protected.
- Convenience: Transparent access to store data simplifies state management across worker and client.
- Flexibility: The store is easily integrated with middleware, allowing dynamic state updates based on request handling.
Elegant Routing
The routing system of React Edge is inspired by Hono, but with enhanced features for SSR:
const router: App.Router = {
routes: [
routerBuilder.routeGroup({
path: '/dashboard',
// Middlewares applied to all routes in the group
middlewares: [authMiddleware, dashboardMiddleware],
routes: [
routerBuilder.route({
path: '/',
handler: {
page: {
value: DashboardPage,
// Specific headers for this route
headers: new Headers({
'Cache-Control': 'private, max-age=0'
})
}
}
}),
routerBuilder.route({
path: '/api/stats',
handler: {
// Routes can return direct responses
response: async (ctx) => {
const stats = await ctx.rpc.stats.getDashboardStats();
return {
value: Response.json(stats),
// Cache for 5 minutes
cache: { ttl: 300 }
};
}
}
})
]
})
]
};
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.
- Built-in Caching: Simplify caching strategies with ttl and tags.
Benefits
- Consistency: By grouping related routes, you ensure consistent middleware application and code organization.
- Scalability: The system supports nested and modular routing for large-scale applications.
- Performance: Native support for caching ensures optimal response times without manual configurations.
Distributed Cache with Edge Cache
React Edge includes a powerful caching system that works seamlessly for both JSON data and entire pages. This caching system supports intelligent tagging and prefix-based invalidation, making it suitable for a wide range of scenarios.
Example: Caching API Responses with Tags
class ProductsAPI extends Rpc {
async getProducts(category: string) {
const products = await this.db.products.findByCategory(category);
return this.createResponse(products, {
cache: {
ttl: 3600, // 1 hour
tags: [`category:${category}`, 'products'],
},
});
}
async updateProduct(id: string, data: ProductData) {
await this.db.products.update(id, data);
// Invalidate specific product cache and its category
await this.cache.deleteBy({
tags: [`product:${id}`, `category:${data.category}`],
});
}
async searchProducts(query: string) {
const results = await this.db.products.search(query);
// Cache with a prefix for easy invalidation
return this.createResponse(results, {
cache: {
ttl: 300, // 5 minutes
tags: [`search:${query}`],
},
});
}
}
// Global cache invalidation
await cache.deleteBy({
// Invalidate all search results
keyPrefix: 'search:',
// And all products in a specific category
tags: ['category:electronics'],
});
Key Features
- Tag-Based Invalidation: Cache entries can be grouped using tags, allowing for easy and selective invalidation when data changes.
- Prefix Matching: Invalidate multiple cache entries using a common prefix, ideal for scenarios like search queries or hierarchical data.
- Time-to-Live (TTL): Set expiration times for cache entries to ensure data freshness while maintaining high performance.
Benefits
- Improved Performance: Reduce load on APIs by serving cached responses for frequently accessed data.
- Scalability: Efficiently handle large-scale datasets and high traffic with a distributed caching system.
- Flexibility: Fine-grained control over caching, enabling developers to optimize performance without sacrificing data accuracy.
Link: The Component That Thinks Ahead
The Link
component is an intelligent and performance-oriented solution for preloading client-side resources, ensuring a smoother and faster navigation experience for users. Its prefetching functionality is triggered when the user hovers over the link, taking advantage of idle moments to request destination data in advance.
How It Works
-
Conditional Prefetching: The
prefetch
attribute (enabled by default) controls whether the preloading is executed. -
Intelligent Cache: A
Set
is used to store already prefetched links, avoiding redundant fetch calls. -
Mouse Enter Event: When the user hovers over the link, the
handleMouseEnter
function checks if preloading is necessary and, if so, starts afetch
request for the destination. - Error Resilience: Any failure during the request is suppressed, ensuring the component's behavior is unaffected by temporary network issues.
Example Usage
<app.Link href="/about" prefetch>
About Us
</app.Link>
When the user hovers over the âAbout Usâ link, the component will start preloading the data for the /about page, ensuring an almost instant transition. Genius idea, isnât it? Inspired by the react.dev documentation.
app.useContext: The Portal to the Edge
The app.useContext
hook is a cornerstone of React Edge, granting seamless access to the worker's entire context. It provides a powerful interface for managing routing, state, RPC calls, and more.
Example: Using app.useContext in a Dashboard
const DashboardPage = () => {
const {
// Current route parameters
pathParams,
// Parsed query parameters
searchParams,
// Matched route
path,
// Original route (with parameters)
rawPath,
// RPC proxy
rpc,
// Shared store
store,
// Full URL
url,
} = app.useContext<App.Context>();
// Fully typed RPC call
const { data } = app.useFetch(
async (ctx) => ctx.rpc.getDashboardStats()
);
// Accessing shared store data
const user = store.get('user');
return (
<div>
<h1>Welcome to your dashboard, {user.name}</h1>
<p>Currently viewing: {path}</p>
</div>
);
};
Key Features of app.useContext
- Route Management: Gain access to the matched route, its parameters, and query strings effortlessly.
- RPC Integration: Make typed and secure RPC calls directly from the client with no additional configuration.
- Shared Store Access: Retrieve or set values in the shared worker-client state with complete 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 boilerplate. This simplifies complex applications, making them easier to maintain and faster to develop.
app.useUrlState: State Synced with the URL
The app.useUrlState hook keeps your application state in sync with the URL query parameters, offering fine-grained control over what is included in the URL, how the state is serialized, and when it updates.
const ProductsPage = () => {
// State synced with URL automatically
const [filters, setFilters] = app.useUrlState({
category: 'all',
minPrice: 0,
maxPrice: 1000,
other: {
foo: 'bar',
baz: 'qux'
}
}, {
debounce: 500, // Debounce URL updates by 500ms
kebabCase: true, // Converts keys to kebab-case for cleaner URLs
omitKeys: ['filter.locations'], // Exclude specific keys from the URL
omitValues: [], // Exclude specific values from the URL
pickKeys: [], // Include only specific keys in the URL
prefix: '', // Add an optional prefix to query keys
url: ctx.url // Use the current URL from the context (works server side)
});
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>
);
};
Parameters
-
Initial State
- An object defining the default structure and values for your state.
-
Options:
- debounce: Controls how quickly the URL is updated after state changes. Useful for preventing excessive updates.
- kebabCase: Converts state keys to kebab-case when serializing to the URL (e.g., filter.locations â filter-locations).
- omitKeys: Specifies keys to exclude from the URL. For example, sensitive data or large objects can be omitted.
- omitValues: Values that, when present, will exclude the associated key from the URL.
- pickKeys: Limits the serialized state to only include specified keys.
- prefix: Adds a prefix to all query parameters for namespacing.
- url: The base URL to sync with, typically derived from the app context.
Benefits
- Identical useState API: Easy integration with existing components.
- SEO-Friendly: Ensures state-dependent views are reflected in sharable and bookmarkable URLs.
- Debounced Updates: Prevents excessive query updates for rapidly changing inputs, like sliders or text boxes.
- Clean URLs: Options like kebabCase and omitKeys keep query strings readable and relevant.
- State Hydration: Automatically initializes state from the URL on component mount, making deep linking seamless.
- Works Everywhere: Supports server-side rendering and client-side navigation, ensuring consistent state across the application.
Practical Applications
- Filters for Property Listings: Sync user-applied filters like listingTypes and map bounds to the URL for sharable searches.
- Dynamic Views: Ensure map zoom, center points, or other view settings persist across page refreshes or links.
- User Preferences: Save user-selected settings in the URL for easy sharing or bookmarking.
app.useStorageState: Persistent State
The app.useStorageState
hook allows you to persist state in the browser using localStorage
or sessionStorage
, with full TypeScript support.
type RecentSearch = {
term: string;
date: string;
category: string;
}
type SearchState = {
recentSearches: RecentSearch[];
favorites: string[];
lastSearch?: string;
}
const RecentSearches = () => {
// Initialize state with typing and default values
const [searchState, setSearchState] = app.useStorageState<SearchState>(
// Unique key for storage
'user-searches',
// Initial state
{
recentSearches: [],
favorites: [],
lastSearch: undefined
},
// Configuration options
{
// Delays saving to storage by 500ms to optimize performance
debounce: 500,
// Sets the storage type:
// 'local' - persists even after closing the browser
// 'session' - persists only during the session
storage: 'local',
// Exclude these keys when saving to storage
omitKeys: ['lastSearch'],
// Save only these specific keys
pickKeys: ['recentSearches', 'favorites']
}
);
return (
<div className="recent-searches">
<h3>Recent Searches</h3>
{/* List recent searches */}
<ul>
{searchState.recentSearches.map((search, index) => (
<li key={index}>
<span>{search.term}</span>
<small>{search.date}</small>
{/* Button to favorite search */}
<button
onClick={() => {
setSearchState({
...searchState,
favorites: [...searchState.favorites, search.term]
})
}}
>
â
</button>
</li>
))}
</ul>
{/* Button to clear history */}
<button
onClick={() => {
setSearchState({
...searchState,
recentSearches: []
})
}}
>
Clear History
</button>
</div>
);
};
Persistence Options
- debounce: Controls the frequency of saves to storage.
- storage: Choose between localStorage and sessionStorage.
- omitKeys/pickKeys: Fine-grained control over which data is persisted.
Performance
- Optimized updates with debounce.
- Automatic serialization/deserialization.
- In-memory caching.
Common Use Cases
- Search history
- Favorites list
- User preferences
- Filter state
- Temporary shopping cart
- Draft forms
app.useDebounce: Frequency Control
Debounce reactive values effortlessly:
const SearchInput = () => {
const [input, setInput] = useState('');
// Debounced value updates only after 300ms of 'silence'
const debouncedValue = app.useDebounce(input, 300);
const { data } = app.useFetch(
async (ctx) => ctx.rpc.search(debouncedValue),
{
// Fetch occurs only when the debounced value changes
deps: [debouncedValue]
}
);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder='Search...'
/>
<SearchResults data={data} />
</div>
);
};
app.useDistinct: State Without Duplicates
Keep arrays with unique values while maintaining type safety.
The app.useDistinct
hook specializes in detecting when a value has truly changed, with support for deep comparison and debounce:
const SearchResults = () => {
const [search, setSearch] = useState('');
// Detect distinct changes in the search value
const {
value: currentSearch, // Current value
prevValue: lastSearch, // Previous value
distinct: hasChanged // Indicates if a distinct change occurred
} = app.useDistinct(search, {
// Debounce by 300ms
debounce: 300,
// Enable deep comparison
deep: true,
// Custom comparison function
compare: (a, b) => a?.toLowerCase() === b?.toLowerCase()
});
// Fetch only when the search value has distinctly changed
const { data } = app.useFetch(
async (ctx) => ctx.rpc.search(currentSearch),
{
deps: [currentSearch],
// Execute fetch only if there's a distinct change
shouldFetch: () => hasChanged
}
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
{hasChanged && (
<small>
Search updated from '{lastSearch}' to '{currentSearch}'
</small>
)}
<SearchResults data={data} />
</div>
);
};
Key Features
- Distinct Value Detection:
- Tracks the current and previous values.
- Automatically detects if a change is meaningful based on your criteria.
- Deep Comparison:
- Enables value equality checks at a deep level for complex objects.
- Custom Comparison:
- Supports custom functions to define what constitutes a âdistinctâ change.
- Debounced:
- Reduces unnecessary updates when changes occur too frequently.
Benefits
- Identical useState API: Easy integration with existing components.
- Optimized Performance: Avoids unnecessary re-fetching or re-computation when the value hasnât meaningfully changed.
- Enhanced UX: Prevents overreactive UI updates, leading to smoother interactions.
- Simplified Logic: Eliminates manual checks for equality or duplication in state management.
The hooks in React Edge are designed to work in harmony, providing a fluid and strongly typed development experience. Their combination allows for creating complex and reactive interfaces with much less code.
The React Edge CLI: Power at Your Fingertips
The CLI for React Edge was designed to simplify developers' lives by gathering essential tools into a single, intuitive interface. Whether you're a beginner or an experienced developer, the CLI ensures you can configure, develop, test, and deploy projects efficiently and effortlessly.
Key Features
Modular and Flexible Commands:
- build: Builds both the app and the worker, with options to specify environments and modes (development or production).
- dev: Starts local or remote development servers, allowing separate work on the app or worker.
- deploy: Enables fast and efficient deployments leveraging the combined power of Cloudflare Workers and Cloudflare R2, ensuring performance and scalability in edge infrastructure.
- logs: Monitors worker logs directly in the terminal.
- lint: Automates Prettier and ESLint execution, with support for auto-fixes.
- test: Runs tests with optional coverage using Vitest.
- type-check: Validates TypeScript typing across the project.
Real-World Use Cases
Iâm proud to share that the first production application using React Edge is already live! It's a Brazilian real estate company, Lopes ImĂłveis, which is already reaping the benefits of the framework's performance and flexibility.
On their website, properties are loaded into cache to optimize search and provide a smoother user experience. Since it's a highly dynamic site, route caching uses a TTL of just 10 seconds, combined with the stale-while-revalidate
strategy. This ensures the site delivers updated data with exceptional performance, even during background revalidations.
Additionally, recommendations for similar properties are calculated efficiently and asynchronously in the background, then saved directly to Cloudflare's cache using the integrated RPC caching system. This approach reduces response times for subsequent requests and makes querying recommendations nearly instantaneous. All images are stored on Cloudflare R2, offering scalable and distributed storage without relying on external providers.
Soon, weâll also launch a massive automated marketing project for Easy Auth, showcasing the potential of this technology even further.
Conclusion
And so, dear readers, weâve reached the end of this journey through the world of React Edge! I know thereâs still a sea of incredible features to explore, like simpler authentication options such as Basic and Bearer, and other tricks that make a developerâs day much happier. But hold on! The idea is to bring more detailed articles in the future to dive deep into each of these features.
Spoiler alert: soon, React Edge will be open source and properly documented! Balancing development, work, writing, and a little bit of social life isnât easy, but the excitement of seeing this marvel in action, especially with the absurd speed provided by Cloudflare's infrastructure, is the fuel that keeps me going. So, stay tuned, because the best is yet to come! đ
In the meantime, if you want to start exploring and testing it right now, the package is already available on NPM: React Edge on NPM..
My email is feliperohdee@gmail.com, and Iâm always open to feedbackâthis is just the beginning of this journey. Suggestions and constructive criticism are welcome. If you enjoyed what you read, share it with your friends and colleagues, and stay tuned for more updates. Thank you for reading this far, and see you next time! đđđ
Top comments (1)
Really interesting, but lack a documentation website (even just in Markdown format in repo), and possibly a github repo, at least for reporting issues.