React gRPC-Web — Building Real-Time React Apps with gRPC Backends
In this tutorial, you will learn about React gRPC. We cover key concepts, practical examples, and best practices to help you master this topic.
React gRPC-Web combines React's component model with gRPC-Web's real-time streaming capabilities, enabling responsive dashboards and live-updating UIs backed by efficient protobuf-based APIs.
What You'll Learn
- Generating TypeScript gRPC-Web clients from protobuf
- React hooks for gRPC unary calls
- React hooks for server-streaming
- State management with gRPC data
- Error handling and reconnection in React
- Performance optimization for gRPC-Web in React
Why It Matters
React applications often use REST or Graphql, which add overhead for simple CRUD. gRPC-Web provides strongly-typed, efficient APIs with built-in streaming support. DodaTech's Durga Antivirus Pro uses React with gRPC-Web for its admin dashboard, reducing payload size by 60% compared to JSON REST APIs.
Real-World Use
A React dashboard shows real-time threat alerts via a gRPC-Web server-streaming call. When a new threat is detected, the server pushes it to all connected dashboards within 200ms. The React component updates the threat list optimistically without any polling.
sequenceDiagram
participant React
participant Envoy
participant gRPC
React->>Envoy: gRPC-Web StreamThreats
Envoy->>gRPC: StreamThreats (HTTP/2)
gRPC-->>Envoy: Threat detected
Envoy-->>React: Threat data
React->>React: setState (new threat)
gRPC-->>Envoy: Threat detected
Envoy-->>React: Threat data
React->>React: setState (new threat)
Code Examples
Example 1: React Hook for gRPC Unary Call
import { useState, useEffect } from "react";
import { ThreatServiceClient } from "./proto/threat.client";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
const transport = new GrpcWebFetchTransport({
baseUrl: process.env.REACT_APP_API_URL || "/grpc",
});
const client = new ThreatServiceClient(transport);
interface UseGrpcQueryOptions<T> {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
enabled?: boolean;
}
function useGrpcQuery<TRequest, TResponse>(
method: (client: any, request: TRequest) => Promise<{ response: TResponse }>,
request: TRequest | null,
options?: UseGrpcQueryOptions<TResponse>
) {
const [data, setData] = useState<TResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!request || options?.enabled === false) {
return;
}
let cancelled = false;
async function fetch() {
setLoading(true);
setError(null);
try {
const { response } = await method(client, request);
if (!cancelled) {
setData(response);
options?.onSuccess?.(response);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
options?.onError?.(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetch();
return () => {
cancelled = true;
};
}, [JSON.stringify(request)]);
return { data, loading, error, refetch: fetch };
}
// Usage in component
function ThreatList() {
const { data, loading, error } = useGrpcQuery(
(c, r) => c.listThreats(r),
{ pageSize: 50, severity: "CRITICAL" }
);
if (loading) return <Spinner />;
if (error) return <ErrorDisplay error={error} />;
return (
<ul>
{data?.threats.map(t => (
<ThreatItem key={t.threatId} threat={t} />
))}
</ul>
);
}
Example 2: React Subscription Hook for Streaming
import { useState, useEffect, useRef, useCallback } from "react";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { ThreatServiceClient } from "./proto/threat.client";
import type { Threat } from "./proto/threat";
const transport = new GrpcWebFetchTransport({
baseUrl: process.env.REACT_APP_API_URL || "/grpc",
fetchInit: {
credentials: "include",
},
});
const client = new ThreatServiceClient(transport);
interface StreamState {
connected: boolean;
lastMessage: Threat | null;
error: Error | null;
}
function useThreatStream(deviceId?: string) {
const [threats, setThreats] = useState<Threat[]>([]);
const [state, setState] = useState<StreamState>({
connected: false,
lastMessage: null,
error: null,
});
const streamRef = useRef<{ cancel: () => void } | null>(null);
const retryTimeoutRef = useRef<number>();
const connect = useCallback(() => {
if (streamRef.current) {
streamRef.current.cancel();
}
setState(s => ({ ...s, connected: true, error: null }));
const stream = client.streamThreats({
deviceId: deviceId || "",
});
stream.responses.onMessage((threat) => {
setThreats(prev => [threat, ...prev].slice(0, 100));
setState(s => ({
...s,
lastMessage: threat,
}));
});
stream.responses.onError((error) => {
console.error("Stream error:", error);
setState(s => ({
...s,
connected: false,
error: error as Error,
}));
// Auto-reconnect after 3 seconds
retryTimeoutRef.current = window.setTimeout(() => {
connect();
}, 3000);
});
stream.responses.onComplete(() => {
setState(s => ({ ...s, connected: false }));
});
streamRef.current = stream;
}, [deviceId]);
useEffect(() => {
connect();
return () => {
if (streamRef.current) {
streamRef.current.cancel();
}
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
}
};
}, [connect]);
return {
threats,
isConnected: state.connected,
lastMessage: state.lastMessage,
error: state.error,
reconnect: connect,
};
}
// Component using the subscription
function ThreatDashboard() {
const {
threats,
isConnected,
error,
reconnect,
} = useThreatStream();
return (
<div className="dashboard">
<div className="status-bar">
<span className={`indicator ${isConnected ? "connected" : "disconnected"}`} />
{isConnected ? "Connected" : "Disconnected"}
{error && (
<button onClick={reconnect}>Reconnect</button>
)}
</div>
<div className="threat-feed">
{threats.map(threat => (
<ThreatCard key={threat.threatId} threat={threat} />
))}
</div>
</div>
);
}
Example 3: Protobuf Generation for React
{
"scripts": {
"proto:generate": "protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--js_out=import_style=commonjs,binary:src/proto \
--ts_out=service=grpc-web:src/proto \
--proto_path=../protos \
../protos/**/*.proto"
},
"dependencies": {
"@protobuf-ts/grpcweb-transport": "^2.9.0",
"@protobuf-ts/runtime": "^2.9.0",
"@protobuf-ts/runtime-rpc": "^2.9.0"
}
}
// package.json scripts
// "proto:gen": "buf generate ../protos --template buf.gen.yaml"
// buf.gen.yaml
// version: v1
// plugins:
// - plugin: es
// out: src/proto
// opt: target=ts
// - plugin: connect-es
// out: src/proto
// opt: target=ts
Common Mistakes
- Not handling stream reconnection — browser tabs can go to sleep, network can change. Implement automatic reconnection with exponential backoff.
- Forgetting to cancel streams on unmount — if a component unmounts without canceling its stream, the connection leaks. Always cancel in useEffect cleanup.
- Mixing REST and gRPC-Web URLs — gRPC-Web needs a different URL (Envoy proxy endpoint) than REST. Configure baseUrl correctly per environment.
- Using complex state management for stream data — for simple cases, React state + useEffect is sufficient. For complex cases, use Zustand or Jotai.
- Ignoring binary vs JSON format — gRPC-Web can use binary (smaller) or JSON (debuggable). Use binary in production, JSON in development.
Practice Questions
- How do you generate TypeScript gRPC-Web clients from protobuf?
- What is the pattern for React hooks with gRPC streaming?
- How do you handle reconnection in gRPC-Web streams?
- Why should you cancel streams in useEffect cleanup?
- How does gRPC-Web state management differ from REST?
Challenge: Build a React dashboard with 3 gRPC-Web streams: threat alerts, device status changes, and scan progress. Each stream uses a custom hook with reconnection logic. The dashboard displays connection status per stream and auto-reconnects on failure.
Mini Project
Build a complete React + gRPC-Web application with: generated TypeScript clients from protobuf, custom hooks for unary and streaming calls, connection status indicators, auto-reconnection with exponential backoff, and a real-time threat dashboard with live updates.
FAQ
What's Next
Learn about gRPC-Web fundamentals
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro