@web3-onboard/solid for SolidStart

I want to use this package on my solidstart app but there are many issues. First I need ssr disabled otherwise I get error localstorage is not defined on app start Sometimes it runs the sign message twice It won't redirect to /dashboard as expected and says localstorage is not defined see files below repo of package: https://github.com/thirdweb-dev/web3-onboard
12 Replies
Atila
Atila3d ago
that's because localStorage isn't available on the server-side. this library is client-side only, so you can use it in a component with clientOnly() or wrap it around a check. 'localStorage' in window or simply window
Thomas
ThomasOP3d ago
Yeah I figured that was the reason... So I can still use it you say? Because there's init On entry client I just do isserver? Will test
Atila
Atila3d ago
yes, the important part is ensuring the lib will only run on the client-side. There are many ways to make that
Thomas
ThomasOP3d ago
yeah so I've enable again ssr since we can actually make the lib client only here's my code Context.tsx
import {
createContext,
createEffect,
Show,
useContext,
type ParentProps,
} from "solid-js";
import {
type AccessorWithLatest,
useLocation,
createAsync,
useAction,
useSearchParams,
} from "@solidjs/router";
import { useOnboard } from "@web3-onboard/solid";
import { protectedRoute, logout, querySession, type Session } from "~/session";
import { sign, login } from "~/session/web3";

const Context = createContext<{
session: AccessorWithLatest<Session | null | undefined>;
signedIn: () => boolean;
signOut: () => Promise<never>;
}>();

export default function Auth(props: ParentProps) {
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const session = createAsync(() => querySession(location.pathname), {
deferStream: true,
});
const signOut = useAction(logout);
const signedIn = () => Boolean(session()?.id);

const web3Login = useAction(login);
const { connectWallet, connectedWallet } = useOnboard();

createEffect(async () => {
if (searchParams.login === "true" && !signedIn() && window) {
try {
await connectWallet();
const wallet = connectedWallet();
if (!wallet) throw new Error("Wallet connection failed");
const address = await sign(wallet.provider);
const r = searchParams.redirect;
return await web3Login(address, Array.isArray(r) ? r[0] : r);
} catch (err: unknown) {
if (err instanceof Error) setSearchParams({ error: err.message });
} finally {
setSearchParams({ login: undefined });
}
}
});

return (
<Context.Provider value={{ session, signedIn, signOut }}>
<Show when={!protectedRoute(location.pathname) || signedIn()}>
{props.children}
</Show>
</Context.Provider>
);
}

export function useSession() {
const context = useContext(Context);
if (!context)
throw new Error("useSession must be used within Session context");
return context;
}
import {
createContext,
createEffect,
Show,
useContext,
type ParentProps,
} from "solid-js";
import {
type AccessorWithLatest,
useLocation,
createAsync,
useAction,
useSearchParams,
} from "@solidjs/router";
import { useOnboard } from "@web3-onboard/solid";
import { protectedRoute, logout, querySession, type Session } from "~/session";
import { sign, login } from "~/session/web3";

const Context = createContext<{
session: AccessorWithLatest<Session | null | undefined>;
signedIn: () => boolean;
signOut: () => Promise<never>;
}>();

export default function Auth(props: ParentProps) {
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const session = createAsync(() => querySession(location.pathname), {
deferStream: true,
});
const signOut = useAction(logout);
const signedIn = () => Boolean(session()?.id);

const web3Login = useAction(login);
const { connectWallet, connectedWallet } = useOnboard();

createEffect(async () => {
if (searchParams.login === "true" && !signedIn() && window) {
try {
await connectWallet();
const wallet = connectedWallet();
if (!wallet) throw new Error("Wallet connection failed");
const address = await sign(wallet.provider);
const r = searchParams.redirect;
return await web3Login(address, Array.isArray(r) ? r[0] : r);
} catch (err: unknown) {
if (err instanceof Error) setSearchParams({ error: err.message });
} finally {
setSearchParams({ login: undefined });
}
}
});

return (
<Context.Provider value={{ session, signedIn, signOut }}>
<Show when={!protectedRoute(location.pathname) || signedIn()}>
{props.children}
</Show>
</Context.Provider>
);
}

export function useSession() {
const context = useContext(Context);
if (!context)
throw new Error("useSession must be used within Session context");
return context;
}
app.config.ts
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
server: { preset: "vercel" },
vite: {
plugins: [tailwindcss()],
ssr: {
external: [
"drizzle-orm",
"ethers",
"@web3-onboard/core", //I put those here ?
"@web3-onboard/injected-wallets",
"@web3-onboard/solid",
],
},
},
});
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
server: { preset: "vercel" },
vite: {
plugins: [tailwindcss()],
ssr: {
external: [
"drizzle-orm",
"ethers",
"@web3-onboard/core", //I put those here ?
"@web3-onboard/injected-wallets",
"@web3-onboard/solid",
],
},
},
});
entry-client.tsx
import { mount, StartClient } from "@solidjs/start/client";
import { init } from "@web3-onboard/solid";
import injected from "@web3-onboard/injected-wallets";

if (window) {
init({
wallets: [injected()],
chains: [
{
token: "ETH",
id: "0xaa36a7",
label: "Sepolia Testnet",
rpcUrl: "https://rpc.sepolia.org",
},
],
});
}

mount(() => <StartClient />, document.getElementById("app")!);
import { mount, StartClient } from "@solidjs/start/client";
import { init } from "@web3-onboard/solid";
import injected from "@web3-onboard/injected-wallets";

if (window) {
init({
wallets: [injected()],
chains: [
{
token: "ETH",
id: "0xaa36a7",
label: "Sepolia Testnet",
rpcUrl: "https://rpc.sepolia.org",
},
],
});
}

mount(() => <StartClient />, document.getElementById("app")!);
and I get 503 Server Unavailable ReferenceError: localStorage is not defined what am I missing?
Atila
Atila3d ago
useOnboard seems to still be initialized in the server
Thomas
ThomasOP3d ago
yeah so how do I run it only on client? what's the best way?
Atila
Atila3d ago
I'm not super familiar wiht the app and I have never used the lib, but perhaps move it out of the page component and wrap its dedicate one in a clientOnly wrapper.
Thomas
ThomasOP3d ago
yeah u mean moving out of the context and have it as function for web3 things with createEffect? but components must have jsx return value right? if I call the client only comp in app.tsx with {myclientonly()} will it work?
Atila
Atila3d ago
the clientOnly is a dynamic import that will only resolve on the client. On the server it will work as a passthrough - so if there's anything in your rendering logic that depend on it, things will break
Thomas
ThomasOP3d ago
ok I get error Malformed server function stream header: !DOCTYPE h my web3.tsx comp
import { createEffect } from "solid-js";
import { useSearchParams, useAction } from "@solidjs/router";
import { useOnboard } from "@web3-onboard/solid";
import { sign, login } from "~/session/web3";
import { useSession } from "./Context";

export default function Web3Login() {
const [searchParams, setSearchParams] = useSearchParams();
const web3Login = useAction(login);
const { connectWallet, connectedWallet } = useOnboard();
const { signedIn } = useSession();

createEffect(async () => {
if (searchParams.login === "true" && !signedIn()) {
try {
await connectWallet();
const wallet = connectedWallet();
if (!wallet) throw new Error("Wallet connection failed");
const address = await sign(wallet.provider);
const r = searchParams.redirect;
await web3Login(address, Array.isArray(r) ? r[0] : r);
} catch (err: unknown) {
if (err instanceof Error) setSearchParams({ error: err.message });
} finally {
setSearchParams({ login: undefined });
}
}
});

return null;
}
import { createEffect } from "solid-js";
import { useSearchParams, useAction } from "@solidjs/router";
import { useOnboard } from "@web3-onboard/solid";
import { sign, login } from "~/session/web3";
import { useSession } from "./Context";

export default function Web3Login() {
const [searchParams, setSearchParams] = useSearchParams();
const web3Login = useAction(login);
const { connectWallet, connectedWallet } = useOnboard();
const { signedIn } = useSession();

createEffect(async () => {
if (searchParams.login === "true" && !signedIn()) {
try {
await connectWallet();
const wallet = connectedWallet();
if (!wallet) throw new Error("Wallet connection failed");
const address = await sign(wallet.provider);
const r = searchParams.redirect;
await web3Login(address, Array.isArray(r) ? r[0] : r);
} catch (err: unknown) {
if (err instanceof Error) setSearchParams({ error: err.message });
} finally {
setSearchParams({ login: undefined });
}
}
});

return null;
}
I call it in app.tsx const Web3LoginComp = clientOnly(() => import("./components/Web3"));
[vite] (ssr) Error when evaluating SSR module /website/src/session/web3.ts?tsr-directive-use-server=: Cannot read properties of undefined (reading 'query')
[vite] (ssr) Error when evaluating SSR module /website/src/session/web3.ts?tsr-directive-use-server=: Cannot read properties of undefined (reading 'query')
Atila
Atila3d ago
Hard to spot the issue looking at the snippets. If the clinetOnly import is like you pasted, and the usage is like below - that isn't exactly the problem. maybe start with a more minimal implementation and see what part of the lib/integration is causing it
const ClientCounter = clientOnly(() => import("./components/Counter"));

export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
<ClientCounter />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
const ClientCounter = clientOnly(() => import("./components/Counter"));

export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
<ClientCounter />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
Thomas
ThomasOP3d ago
yeah there's the <Auth> wrapper just like in with-auth example but it's the same maybe there's an error because I useAction inside a client only comp?? my login is just
export const login = action(async (wallet: string, redirectTo?: string) => {
"use server";
const { id } = await getUserByWallet(wallet);
const session = await getSession();
await session.update({ id, wallet });
return safeRedirect(redirectTo);
});
export const login = action(async (wallet: string, redirectTo?: string) => {
"use server";
const { id } = await getUserByWallet(wallet);
const session = await getSession();
await session.update({ id, wallet });
return safeRedirect(redirectTo);
});
definitely issue is the server action I tested to removed it and it works @Atila yeah solved issue just some bad config with db thanks only small issue left is that it runs 2 times this part const address = await sign(wallet.provider); a reactivity issue? if I console.log wallet in login it logs wallet 2 times

Did you find this page helpful?