TypeScript-first expansion pack for TanStack Query that gives you Protobuf superpowers.
TypeScript-first expansion pack for TanStack Query that gives you Protobuf superpowers.
Connect-Query is an wrapper around TanStack Query (react-query), written in TypeScript and thoroughly tested. It enables effortless communication with servers that speak the Connect Protocol.
TransportProvideruseTransportuseQueryuseSuspenseQueryuseInfiniteQueryuseSuspenseInfiniteQueryuseMutationcreateConnectQueryKeycallUnaryMethodcreateProtobufSafeUpdatercreateQueryOptionscreateInfiniteQueryOptionsaddStaticKeyToTransportConnectQueryKeyQueryOptionsQueryOptionsWithSkipTokenInfiniteQueryOptionsInfiniteQueryOptionsWithSkipTokennpm install @connectrpc/connect-query @connectrpc/connect-web
[!TIP]
If you are using something that doesn't automatically install peerDependencies (npm older than v7), you'll want to make sure you also have
@bufbuild/protobuf,@connectrpc/connect, and@tanstack/react-queryinstalled.@connectrpc/connect-webis required for defining the transport to be used by the client.
Connect-Query will immediately feel familiar to you if you've used TanStack Query. It provides a similar API, but instead takes a definition for your endpoint and returns a typesafe API for that endpoint.
First, make sure you've configured your provider and query client:
import { createConnectTransport } from "@connectrpc/connect-web";
import { TransportProvider } from "@connectrpc/connect-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const finalTransport = createConnectTransport({
baseUrl: "https://demo.connectrpc.com",
});
const queryClient = new QueryClient();
function App() {
return (
);
}
With configuration completed, you can now use the useQuery hook to make a request:
import { useQuery } from '@connectrpc/connect-query';
import { say } from 'your-generated-code/eliza-ElizaService_connectquery';
export const Example: FC = () => {
const { data } = useQuery(say, { sentence: "Hello" });
return
;
};
That's it!
The code generator does all the work of turning your Protobuf file into something you can easily import. TypeScript types all populate out-of-the-box. Your documentation is also converted to TSDoc.
One of the best features of this library is that once you write your schema in Protobuf form, the TypeScript types are generated and then inferred. You never again need to specify the types of your data since the library does it automatically.
To make a query, you need a schema for a remote procedure call (RPC). A typed schema can be generated with protoc-gen-es. It generates an export for every service:
/**
* @generated from service connectrpc.eliza.v1.ElizaService
*/
export declare const ElizaService: GenService;
protoc-gen-connect-query is an optional additional plugin that exports every RPC individually for convenience:
import { ElizaService } from "./eliza_pb";
/**
* Say is a unary RPC. Eliza responds to the prompt with a single sentence.
*
* @generated from rpc connectrpc.eliza.v1.ElizaService.Say
*/
export const say: (typeof ElizaService)["method"]["say"];
For more information on code generation, see the documentation for protoc-gen-connect-query and the documentation for protoc-gen-es.
TransportProviderconst TransportProvider: FC
>;
TransportProvider is the main mechanism by which Connect-Query keeps track of the Transport used by your application.
Broadly speaking, "transport" joins two concepts:
baseUrl, but there are also other potentially critical options like request credentials, wire serialization options, or protocol-specific options like Connect's support for HTTP GET.With these two pieces of information in hand, the transport provides the critical mechanism by which your app can make network requests.
To learn more about the two modes of transport, take a look at the Connect-Web documentation on choosing a protocol.
To get started with Connect-Query, simply import a transport (either createConnectTransport or createGrpcWebTransport from @connectrpc/connect-web) and pass it to the provider.
A common use case for the transport is to add headers to requests (like auth tokens, etc). You can do this with a custom interceptor.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TransportProvider } from "@connectrpc/connect-query";
const queryClient = new QueryClient();
export const App = () => {
const transport = createConnectTransport({
baseUrl: "",
interceptors: [
(next) => (request) => {
request.header.append("some-new-header", "some-value");
// Add your headers here
return next(request);
},
],
});
return (
);
};
For more details about what you can do with the transport, see the Connect-Web documentation.
useTransportconst useTransport: () => Transport;
Use this helper to get the default transport that's currently attached to the React context for the calling component.
[!TIP]
All hooks accept a
transportin the options. You can use the Transport from the context, or create one dynamically. If you create a Transport dynamically, make sure to memoize it, because it is taken into consideration when building query keys.
useQueryfunction useQuery,
>(
schema: DescMethodUnary,
input?: SkipToken | MessageInitShape,
{ transport, ...queryOptions }: UseQueryOptions = {},
): UseQueryResult;
The useQuery hook is the primary way to make a unary request. It's a wrapper around TanStack Query's useQuery hook, but it's preconfigured with the correct queryKey and queryFn for the given method.
Any additional options you pass to useQuery will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports.
useSuspenseQueryIdentical to useQuery but mapping to the useSuspenseQuery hook from TanStack Query. This includes the benefits of narrowing the resulting data type (data will never be undefined).
useInfiniteQueryfunction useInfiniteQuery,
>(
schema: DescMethodUnary,
input: SkipToken | MessageInitWithPageParam,
{
transport,
pageParamKey,
getNextPageParam,
...queryOptions
}: UseInfiniteQueryOptions,
): UseInfiniteQueryResult>, ConnectError>;
The useInfiniteQuery is a wrapper around TanStack Query's useInfiniteQuery hook, but it's preconfigured with the correct queryKey and queryFn for the given method.
There are some required options for useInfiniteQuery, primarily pageParamKey and getNextPageParam. These are required because Connect-Query doesn't know how to paginate your data. You must provide a mapping from the output of the previous page and getting the next page. pageParamKey supports root keys as strings ("page") and nested keys as dot-separated strings ("query.page"). All other options passed to useInfiniteQuery will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports.
useSuspenseInfiniteQueryIdentical to useInfiniteQuery but mapping to the useSuspenseInfiniteQuery hook from TanStack Query. This includes the benefits of narrowing the resulting data type (data will never be undefined).
useMutationfunction useMutation(
schema: DescMethodUnary,
{ transport, ...queryOptions }: UseMutationOptions = {},
): UseMutationResult, ConnectError, PartialMessage>;
The useMutation is a wrapper around TanStack Query's useMutation hook, but it's preconfigured with the correct mutationFn for the given method.
Any additional options you pass to useMutation will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports.
createConnectQueryKeyThis function is used under the hood of useQuery and other hooks to compute a queryKey for TanStack Query. You can use it to create keys yourself to filter queries.
useQuery creates a query key with the following parameters:
The DataTag type allows @tanstack/react-query functions to properly infer the type of the data returned by the query. This is useful for things like QueryClient.setQueryData and QueryClient.getQueryData.
To create the same key manually, you simply provide the same parameters:
…
You can create a partial key that matches all RPCs of a service:
import { createConnectQueryKey } from "@connectrpc/connect-query";
import { ElizaService } from "./gen/eliza_pb";
const queryKey = createConnectQueryKey({
schema: ElizaService,
cardinality: "finite",
});
// queryKey:
[
"connect-query",
{
serviceName: "connectrpc.eliza.v1.ElizaService",
cardinality: "finite",
},
];
Infinite queries have distinct keys. To create a key for an infinite query, use the parameter cardinality:
import { createConnectQueryKey } from "@connectrpc/connect-query";
import { ListService } from "./gen/list_pb";
// The hook useInfiniteQuery() creates a query
No open issues yet, or sync has not completed.