rocawear
rocawear2y ago

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
rocawear
rocawearOP2y ago
Server:
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");
Client:
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;
If I remove this part it renders content but obv does not work so the error comes from here:
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);
},
});
Nick
Nick2y ago
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
rocawear
rocawearOP2y ago
Yep, I actually moved those there just to make it clearer. I have util/trpc.ts from where I normally would use it
Nick
Nick2y ago
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
rocawear
rocawearOP2y ago
Hey thanks! It works! 🙂
Rammstein
Rammstein2y ago
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
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,
});
__app.tsx
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);
trpc/[trpc].ts
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,
});
trpc.ts
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;
this is just a test page i am using /pages/test/trpc-test.tsx
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;