How Do I Update Client Signals When Using "use server";

I've set up a Supabase AuthService that tracks the error state with a signal.
export const [supabaseError, setSupabaseError] = createSignal("");
export const [supabaseError, setSupabaseError] = createSignal("");
If I get an error from Supabase, I set the error state in my AuthService file. Here's an example from my Logout function:
export async function supabaseLogout() {
try {
// try logging out
const { error } = await supabase.auth.signOut();
// throw an error if there is one
if (error) throw error;
} catch (error: any) {
// THIS WORKS
setSupabaseError(error.message);
}
}
export async function supabaseLogout() {
try {
// try logging out
const { error } = await supabase.auth.signOut();
// throw an error if there is one
if (error) throw error;
} catch (error: any) {
// THIS WORKS
setSupabaseError(error.message);
}
}
Any pages that use the AuthService can import the supabaseError signal, and display error messages when supabaseError().length > 0.
<Show when={supabaseError().length > 0}>
<div>
<p style={{ color: "#c62828" }}>{supabaseError()}</p>
</div>
</Show>
<Show when={supabaseError().length > 0}>
<div>
<p style={{ color: "#c62828" }}>{supabaseError()}</p>
</div>
</Show>
This works great when I do everything on the client. BUT, I'm running into trouble when I have to use "use server";. If I use setSupabaseError() from the server, the DOM does not update. For context, here's my action for deleting an account.
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";

// get the Auth Admin Client
const supabase = getAuthAdminClient();
if (!supabase) {
return;
}
try {
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) throw error;
} catch (error: any) {
// THIS DOESN'T WORK
setSupabaseError(error.message);
}
});
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";

// get the Auth Admin Client
const supabase = getAuthAdminClient();
if (!supabase) {
return;
}
try {
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) throw error;
} catch (error: any) {
// THIS DOESN'T WORK
setSupabaseError(error.message);
}
});
So, how can I update a client-side signal when using "use server";? The docs state the following about Global state and the server: "It is recommended that application state should always be provided via Context instead of relying on global." So, do I need to use context? Meta frameworks are new to me, so please excuse my ignorance 🙂 Thank you! Chris
17 Replies
brenelz
brenelz12mo ago
Return the error message from the action and then use "useSubmission" to get the value So return the error instead of throwing
lxsmnsyc
lxsmnsyc12mo ago
or you can do the try-catch after calling the server action You might be asking how to do it
try {
await runMyServerAction();
} catch (error) {
setError(error);
}
try {
await runMyServerAction();
} catch (error) {
setError(error);
}
ChrisThornham
ChrisThornhamOP12mo ago
@lxsmnsyc 🤖 🤖 and @brenelz I've spent the last hour trying to get your suggestions to work but I'm still stuck. I tried to read about useSubmission, but the docs don't really help. So I tried to return the error.message from the server and then handle the error on the client but I keep getting the following error:
Uncaught (in promise) ReferenceError: $R is not defined
Uncaught (in promise) ReferenceError: $R is not defined
Here's my attempt: Here's how I'm calling the function on the delete-account page:
const useDeleteAccount = useAction(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
const deleteError = await useDeleteAccount(uid);

if (deleteError) {
console.log(deleteError);
setSupabaseError(deleteError);
} else {
const logoutError = await supabaseLogout();

if (logoutError) {
console.log(logoutError);
setSupabaseError(logoutError);
}
}
}
const useDeleteAccount = useAction(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
const deleteError = await useDeleteAccount(uid);

if (deleteError) {
console.log(deleteError);
setSupabaseError(deleteError);
} else {
const logoutError = await supabaseLogout();

if (logoutError) {
console.log(logoutError);
setSupabaseError(logoutError);
}
}
}
And here's my server function:
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
// set loading to true
setSupabaseAuthLoading(true);

// get the Auth Admin Client
const supabaseAdmin = getAuthAdminClient();

if (!supabaseAdmin) {
console.log("Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts");
return null;
}
try {
const { error } = await supabaseAdmin.auth.admin.deleteUser("userId");
if (error) throw error;
return null;
} catch (error: any) {
console.log(`${error.message} from SupabaseAuthService`);
return error.message;
} finally {
// set loading to false
setSupabaseAuthLoading(false);
}
});
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
// set loading to true
setSupabaseAuthLoading(true);

// get the Auth Admin Client
const supabaseAdmin = getAuthAdminClient();

if (!supabaseAdmin) {
console.log("Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts");
return null;
}
try {
const { error } = await supabaseAdmin.auth.admin.deleteUser("userId");
if (error) throw error;
return null;
} catch (error: any) {
console.log(`${error.message} from SupabaseAuthService`);
return error.message;
} finally {
// set loading to false
setSupabaseAuthLoading(false);
}
});
Any help would be appreciated.
lxsmnsyc
lxsmnsyc12mo ago
for the $R the upcoming patch release for Start would fix it. as for the the implementation of your server function, follow my previous message The thing is, if your code has a mix of server and client logic, just split it, where the server logic is in a "use server" function
ChrisThornham
ChrisThornhamOP12mo ago
Thank you. Let me try that on the client. Does my server action look right to you?
lxsmnsyc
lxsmnsyc12mo ago
for example
try {
await doServerStuff();
} catch (error} {
await doClientStuff();
}
try {
await doServerStuff();
} catch (error} {
await doClientStuff();
}
would be
async function wrappedServerStuff() {
"use server";
await doServerStuff();
}

try {
await wrappedServerStuff();
} catch (error) {
await doClientStuff();
}
async function wrappedServerStuff() {
"use server";
await doServerStuff();
}

try {
await wrappedServerStuff();
} catch (error) {
await doClientStuff();
}
Does my server action look right to you?
No
ChrisThornham
ChrisThornhamOP12mo ago
🤦‍♂️ I'm sorry. I really am trying to solve this on my own without asking too many questions. I've literally spent all day on this.
lxsmnsyc
lxsmnsyc12mo ago
@ChrisThornham this explains how to do this properly, which should solve most of your problem
ChrisThornham
ChrisThornhamOP12mo ago
When you say "wrappedServerStuff" does the "wrapped" part mean wrapped in an action? Or can I avoid using an action altogether?
lxsmnsyc
lxsmnsyc12mo ago
The point is if your logic has both server and client logic in it, you move your server logic to a separate function (and you could wrap it with cache or action depending on the intent), and then turn that function into a server function with "use server" this way, your original logic would remain "client-only" while having the ability to run server logic here's probably a much more realistic example
// This function has a mix of server and client logic, but this wouldn't work on the client
async function addUser() {
setLoading(true);
await db.user.insert(someData);
setLoading(false);
}
// This function has a mix of server and client logic, but this wouldn't work on the client
async function addUser() {
setLoading(true);
await db.user.insert(someData);
setLoading(false);
}
The fix:
async function insertUserData(someData) {
"use server";
await db.user.insert(someData)
}

// Much better
async function addUser() {
setLoading(true);
await insertUserData(someData);
setLoading(false);
}
async function insertUserData(someData) {
"use server";
await db.user.insert(someData)
}

// Much better
async function addUser() {
setLoading(true);
await insertUserData(someData);
setLoading(false);
}
ChrisThornham
ChrisThornhamOP12mo ago
Ok... I rewrote everything, but it's still not working. My client function:
const useDeleteAccount = useAction(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
// set loading to true
setSupabaseAuthLoading(true);
try {
// SERVER STUFF
await useDeleteAccount(uid);
} catch (error: any) {
// CLIENT STUFF
console.log(error.message);
setSupabaseError(error.message);
} finally {
setSupabaseAuthLoading(false);
}
}
const useDeleteAccount = useAction(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
// set loading to true
setSupabaseAuthLoading(true);
try {
// SERVER STUFF
await useDeleteAccount(uid);
} catch (error: any) {
// CLIENT STUFF
console.log(error.message);
setSupabaseError(error.message);
} finally {
setSupabaseAuthLoading(false);
}
}
My server function:
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
const supabaseAdmin = getAuthAdminClient();
if (!supabaseAdmin) {
console.log(
"Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts"
);
throw new Error("Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts");
}
try {
const { error } = await supabaseAdmin.auth.admin.deleteUser("userId");
if (error) throw error;
} catch (error) {
throw error;
}
});
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
const supabaseAdmin = getAuthAdminClient();
if (!supabaseAdmin) {
console.log(
"Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts"
);
throw new Error("Unable to initialize Supabase Auth Admin Client in supabaseDeleteAccount() in SupabaseAuthService.ts");
}
try {
const { error } = await supabaseAdmin.auth.admin.deleteUser("userId");
if (error) throw error;
} catch (error) {
throw error;
}
});
Now I'm getting:
POST http://localhost:3000/_server 500 (Internal Server Error) delete-account.tsx:28
POST http://localhost:3000/_server 500 (Internal Server Error) delete-account.tsx:28
lxsmnsyc
lxsmnsyc12mo ago
Okay this looks a lot better now Now it seems to have some server-side errors what does the response on the client look like
ChrisThornham
ChrisThornhamOP12mo ago
TypeError: Cannot read properties of undefined (reading 'length')
TypeError: Cannot read properties of undefined (reading 'length')
From my jsx:
return (
<div>
<h1>Delete Account</h1>
<button
disabled={supabaseAuthLoading()}
onClick={() => handleDelete(userId)}
>
Delete Account
</button>
<Show when={supabaseError().length > 0}>
<div>
<p style={{ color: "#c62828" }}>{supabaseError()}</p>
</div>
</Show>
</div>
);
return (
<div>
<h1>Delete Account</h1>
<button
disabled={supabaseAuthLoading()}
onClick={() => handleDelete(userId)}
>
Delete Account
</button>
<Show when={supabaseError().length > 0}>
<div>
<p style={{ color: "#c62828" }}>{supabaseError()}</p>
</div>
</Show>
</div>
);
And I'm also getting:
Uncaught (in promise) ReferenceError: $R is not defined
Uncaught (in promise) ReferenceError: $R is not defined
lxsmnsyc
lxsmnsyc12mo ago
this one is a common mistake. supabaseError seems to be a resource and so you need to check first if it's not undefined. Usual fix here is the optional chaining syntax as for $R, like I mentioned, upcoming patch release would add a fix for it
ChrisThornham
ChrisThornhamOP12mo ago
So the $R thing is a bug? And yes, supabaseError is a signal. Finally, thank you. I really appreciate your time. I'll keep plugging away at this. @brenelz I've tried 100 combinations using @lxsmnsyc 🤖 suggestions, and it's not working. I've been at this for 6 hours. I'm trying your approach with useSubmission and I'm getting an undefined error when I try to console.log(deleting.result); the result on the client. Here's my simple example. On my delete page in the client:
const useDeleteAccount = useAction(supabaseDeleteAccount);
const deleting = useSubmission(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
useDeleteAccount(uid);
console.log(deleting.result);
}
const useDeleteAccount = useAction(supabaseDeleteAccount);
const deleting = useSubmission(supabaseDeleteAccount);

// function for handling delete account
async function handleDelete(uid: string) {
useDeleteAccount(uid);
console.log(deleting.result);
}
On the server:
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
return userId;
}
export const supabaseDeleteAccount = action(async (userId: string) => {
"use server";
return userId;
}
Do you have any suggestions? Again, I'm sorry for asking so many questions. I've taught myself, React, Solid, the majority of SolidStart, Go, Typescript, CSS etc from docs and tutorials but I keep running into error after error when using "use server" with SolidStart.
ChrisThornham
ChrisThornhamOP12mo ago
@brenelz and @lxsmnsyc 🤖 I went back to the drawing board and straight to the examples in the docs. I followed the "Returning from actions" example here: https://start.solidjs.com/core-concepts/actions#:~:text=Returning%20from%20actions Everything works when I use the example as written. BUT, when I add "use server"; to the echo function, it breaks. Here's how I have it written.
const echo = action(async (message: string) => {
"use server";
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
return message;
});
const echo = action(async (message: string) => {
"use server";
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
return message;
});
The error occurs when calling myEcho in both places:
myEcho("Hello from solid!");
myEcho("Hello from solid!");
AND
setTimeout(() => myEcho("This is a second submission!"), 1500);
setTimeout(() => myEcho("This is a second submission!"), 1500);
Here's the error:
Unrecognized value. Skipped inserting ReferenceError: $R is not defined
at eval (eval at deserialize (index.ts:47:14), <anonymous>:1:1)
Unrecognized value. Skipped inserting ReferenceError: $R is not defined
at eval (eval at deserialize (index.ts:47:14), <anonymous>:1:1)
Here's the full component for clarity:
export default function TestPage() {
const myEcho = useAction(echo);
const echoing = useSubmission(echo);
myEcho("Hello from solid!");
setTimeout(() => myEcho("This is a second submission!"), 1500);
return <p>{echoing.result}</p>;
}
export default function TestPage() {
const myEcho = useAction(echo);
const echoing = useSubmission(echo);
myEcho("Hello from solid!");
setTimeout(() => myEcho("This is a second submission!"), 1500);
return <p>{echoing.result}</p>;
}
So, I'm lost at this point. I'm using SSR... maybe that's the problem? But that's kind of the point of SolidStart, isn't it? NOTE: The docs appear to be wrong. The example calls echo instead of myEcho. So...
echo("Hello from solid!");
// SHOULD BE
myEcho("Hello from solid!");
echo("Hello from solid!");
// SHOULD BE
myEcho("Hello from solid!");
AND
setTimeout(() => echo("This is a second submission!"), 1500);
// SHOULD BE
setTimeout(() => myEcho("This is a second submission!"), 1500);
setTimeout(() => echo("This is a second submission!"), 1500);
// SHOULD BE
setTimeout(() => myEcho("This is a second submission!"), 1500);
I made a commit on Github.
SolidStart Beta Documentation
SolidStart Beta Documentation
Early release documentation and resources for SolidStart Beta
Metru
Metru12mo ago
Just noticed the docs are also contradicting themselves with const myEcho = useAction(echo); but in some cases myEcho is used and others echo is used directly. I'll have to experiment with useAction. I've only used useSubmission, forms (with actions on POST) and createAsync. In both the apps I'm currently using solidstart in, I'm following the pattern from the basic template aka having all "use server" directives in another file. It just makes more sense to me, since you can't pass things from the client scope to it anyways.
Want results from more Discord servers?
Add your server