tRPC sockets with react
Trying to make my React app work with socket with minimal server but getting error:
"Uncaught TypeError: Cannot destructure property 'client' of 'useContext(...)' as it is null."
at Object.useSubscription (createHooksInternal-9c1f8ad9.mjs:363:17)
at createHooksInternal-9c1f8ad9.mjs:57:29
Current code on comments!
7 Replies
Server:
Client:
If I remove this part it renders content but obv does not work so the error comes from here:
import { inferAsyncReturnType, initTRPC } from "@trpc/server";
import {
CreateHTTPContextOptions,
createHTTPServer,
} from "@trpc/server/adapters/standalone";
import {
CreateWSSContextFnOptions,
applyWSSHandler,
} from "@trpc/server/adapters/ws";
import { observable } from "@trpc/server/observable";
import ws from "ws";
// import { z } from "zod";
// This is how you initialize a context for the server
function createContext(
opts: CreateHTTPContextOptions | CreateWSSContextFnOptions
) {
return {};
}
type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();
const publicProcedure = t.procedure;
const router = t.router;
const postRouter = router({
randomNumber: publicProcedure.subscription(() => {
return observable<{ randomNumber: number }>((emit) => {
const timer = setInterval(() => {
emit.next({ randomNumber: Math.random() });
console.log({ randomNumber: Math.random() });
}, 200);
return () => {
clearInterval(timer);
};
});
}),
});
// Merge routers together
export const appRouter = router({
post: postRouter,
});
export type AppRouter = typeof appRouter;
// http server
const { server, listen } = createHTTPServer({
router: appRouter,
createContext,
});
// ws server
const wss = new ws.Server({ server });
applyWSSHandler<AppRouter>({
wss,
router: appRouter,
createContext,
});
setInterval(() => {
console.log("Connected clients", wss.clients.size);
}, 1000);
listen(2022);
console.log("Server running");
import { inferAsyncReturnType, initTRPC } from "@trpc/server";
import {
CreateHTTPContextOptions,
createHTTPServer,
} from "@trpc/server/adapters/standalone";
import {
CreateWSSContextFnOptions,
applyWSSHandler,
} from "@trpc/server/adapters/ws";
import { observable } from "@trpc/server/observable";
import ws from "ws";
// import { z } from "zod";
// This is how you initialize a context for the server
function createContext(
opts: CreateHTTPContextOptions | CreateWSSContextFnOptions
) {
return {};
}
type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();
const publicProcedure = t.procedure;
const router = t.router;
const postRouter = router({
randomNumber: publicProcedure.subscription(() => {
return observable<{ randomNumber: number }>((emit) => {
const timer = setInterval(() => {
emit.next({ randomNumber: Math.random() });
console.log({ randomNumber: Math.random() });
}, 200);
return () => {
clearInterval(timer);
};
});
}),
});
// Merge routers together
export const appRouter = router({
post: postRouter,
});
export type AppRouter = typeof appRouter;
// http server
const { server, listen } = createHTTPServer({
router: appRouter,
createContext,
});
// ws server
const wss = new ws.Server({ server });
applyWSSHandler<AppRouter>({
wss,
router: appRouter,
createContext,
});
setInterval(() => {
console.log("Connected clients", wss.clients.size);
}, 1000);
listen(2022);
console.log("Server running");
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { splitLink, createWSClient, wsLink, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { appRouter } from "../../server/index";
import { useMemo } from "react";
function App() {
const [queryClient] = useState(() => new QueryClient());
const [data, setData] = useState<{ randomNumber: number }>();
const trpc = createTRPCReact<typeof appRouter>();
const trpcClient = useMemo(() => {
const wsClient = createWSClient({ url: "ws://localhost:2022/trpc" });
return trpc.createClient({
links: [
splitLink({
condition: (op) => op.type === "subscription",
true: wsLink({ client: wsClient }),
false: httpBatchLink({ url: "http://localhost:2022/trpc" }),
}),
],
});
}, []);
const subscription = trpc.post.randomNumber.useSubscription(undefined, {
onData(data) {
console.log("received", data);
setData(data);
},
onError(err) {
console.error("error", err);
},
});
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<>
<h1>Hello</h1>
{data}
</>
</QueryClientProvider>
</trpc.Provider>
);
}
export default App;
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { splitLink, createWSClient, wsLink, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { appRouter } from "../../server/index";
import { useMemo } from "react";
function App() {
const [queryClient] = useState(() => new QueryClient());
const [data, setData] = useState<{ randomNumber: number }>();
const trpc = createTRPCReact<typeof appRouter>();
const trpcClient = useMemo(() => {
const wsClient = createWSClient({ url: "ws://localhost:2022/trpc" });
return trpc.createClient({
links: [
splitLink({
condition: (op) => op.type === "subscription",
true: wsLink({ client: wsClient }),
false: httpBatchLink({ url: "http://localhost:2022/trpc" }),
}),
],
});
}, []);
const subscription = trpc.post.randomNumber.useSubscription(undefined, {
onData(data) {
console.log("received", data);
setData(data);
},
onError(err) {
console.error("error", err);
},
});
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<>
<h1>Hello</h1>
{data}
</>
</QueryClientProvider>
</trpc.Provider>
);
}
export default App;
const subscription = trpc.post.randomNumber.useSubscription(undefined, {
onData(data) {
console.log("received", data);
setData(data);
},
onError(err) {
console.error("error", err);
},
});
const subscription = trpc.post.randomNumber.useSubscription(undefined, {
onData(data) {
console.log("received", data);
setData(data);
},
onError(err) {
console.error("error", err);
},
});
const [queryClient] = useState(() => new QueryClient());
const trpc = createTRPCReact<typeof appRouter>();
Probably not the cause but it would be better practice to move these out of the component, you're just creating possible headaches, like createTRPCReact blowing away any state that exists in there on each render pass
Yep, I actually moved those there just to make it clearer. I have util/trpc.ts from where I normally would use it
Okay, well trpc.post.randomNumber.useSubscription will want to use the context API to get the query client and trpc, but it's being called above them in the component heirarchy
Most apps have a Main.tsx and an App.tsx
If you put all your providers in Main, and render App inside Main, then make your calls within App, it will get the context correctly
useContext
A JavaScript library for building user interfaces
Hey thanks! It works! 🙂
Hi I am facing similar problem for a long time now. Can you guys please help me out?
Following are the files
utils/trpc.ts
__app.tsx
trpc/[trpc].ts
trpc.ts
this is just a test page i am using
/pages/test/trpc-test.tsx
import { httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from "../server/routers/_app";
import SuperJSON from "superjson";
function getBaseUrl() {
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
if (process.env.RENDER_INTERNAL_HOSTNAME) {
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
}
return `http://127.0.0.1:${process.env.PORT ?? 3000}`;
}
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
transformer: superjson,
links: [
httpBatchLink({
url: getBaseUrl() + '/api/trpc',
}),
],
};
},
ssr: false,
});
import { httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from "../server/routers/_app";
import SuperJSON from "superjson";
function getBaseUrl() {
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
if (process.env.RENDER_INTERNAL_HOSTNAME) {
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
}
return `http://127.0.0.1:${process.env.PORT ?? 3000}`;
}
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
transformer: superjson,
links: [
httpBatchLink({
url: getBaseUrl() + '/api/trpc',
}),
],
};
},
ssr: false,
});
import '../styles/globals.css';
import type {AppProps, AppType} from 'next/app';
import Theme from '../styles/Theme';
import { trpc } from '../utils/trpc';
const MyApp:AppType=({ Component, pageProps }: AppProps)=> {
return (
<Theme>
<Component {...pageProps} />
</Theme>
);
}
export default trpc.withTRPC(MyApp);
import '../styles/globals.css';
import type {AppProps, AppType} from 'next/app';
import Theme from '../styles/Theme';
import { trpc } from '../utils/trpc';
const MyApp:AppType=({ Component, pageProps }: AppProps)=> {
return (
<Theme>
<Component {...pageProps} />
</Theme>
);
}
export default trpc.withTRPC(MyApp);
import * as trpcNext from '@trpc/server/adapters/next';
import { createContext } from '../../../server/context';
import { appRouter } from '../../../server/routers/_app';
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
});
import * as trpcNext from '@trpc/server/adapters/next';
import { createContext } from '../../../server/context';
import { appRouter } from '../../../server/routers/_app';
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
});
import { Context } from "./context";
import {initTRPC, TRPCError} from '@trpc/server';
import superjson from 'superjson';
import { OpenApiMeta } from 'trpc-openapi';
const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user) {
throw new TRPCError({message:"UNAUTHORIZED",code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const hasCloudmate = t.middleware(({next,ctx})=>{
if (!ctx.cloudmateUserObject || !ctx.cloudmateAsanaObject) {
throw new TRPCError({message:"CLOUDMATE NOT COMPLETELY SETUP",code: "PRECONDITION_FAILED" });
}
if(!ctx.ownerUserObject || !ctx.ownerAsanaObject){
throw new TRPCError({message:"CREATOR USER NOT COMPLETELY SETUP",code: "PRECONDITION_FAILED" });
}
return next();
});
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(isAuthed);
export const middleware = t.middleware;
export const mergeRouters = t.mergeRouters;
import { Context } from "./context";
import {initTRPC, TRPCError} from '@trpc/server';
import superjson from 'superjson';
import { OpenApiMeta } from 'trpc-openapi';
const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user) {
throw new TRPCError({message:"UNAUTHORIZED",code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const hasCloudmate = t.middleware(({next,ctx})=>{
if (!ctx.cloudmateUserObject || !ctx.cloudmateAsanaObject) {
throw new TRPCError({message:"CLOUDMATE NOT COMPLETELY SETUP",code: "PRECONDITION_FAILED" });
}
if(!ctx.ownerUserObject || !ctx.ownerAsanaObject){
throw new TRPCError({message:"CREATOR USER NOT COMPLETELY SETUP",code: "PRECONDITION_FAILED" });
}
return next();
});
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(isAuthed);
export const middleware = t.middleware;
export const mergeRouters = t.mergeRouters;
import { trpc } from "../../utils/trpc";
import { useEffect, useState } from "react";
const TestPage = () => {
const [userData, setUserData] = useState<any>();
const [orgData, setOrgData] = useState<any>();
const { data: users } = trpc.user.listUsers.useQuery();
const { data: organizations } = trpc.workspace.getWorkspaces.useQuery({});
console.log("Orgs: ", organizations);
useEffect(() => {
setUserData(users);
setOrgData(organizations);
console.log("user state", userData);
}, [users, userData, organizations, orgData]);
if (!users) return;
return (
<div>
</div>
);
};
export default TestPage;
import { trpc } from "../../utils/trpc";
import { useEffect, useState } from "react";
const TestPage = () => {
const [userData, setUserData] = useState<any>();
const [orgData, setOrgData] = useState<any>();
const { data: users } = trpc.user.listUsers.useQuery();
const { data: organizations } = trpc.workspace.getWorkspaces.useQuery({});
console.log("Orgs: ", organizations);
useEffect(() => {
setUserData(users);
setOrgData(organizations);
console.log("user state", userData);
}, [users, userData, organizations, orgData]);
if (!users) return;
return (
<div>
</div>
);
};
export default TestPage;