skip to Main Content

I wanted to know if this method is optimal or not. Because I did not find any documentation about it.

example

a server action to get the current user information

actions/whoami.ts

"use server";

import { auth, prisma } from "@/lib/auth"; // next-auth config file

export default async function whoami(): Promis<User | null> {
    const session = await auth();

    if (!session?.user || !session.user.email) {
        return null;
    }

    const user = await prisma.user.findUnique({
        where: {
            email: session.user.email,
        },
    });

    return user;
}

use in client components:

export default function ClientPage() {
    const { data: user } = useSWR("whoami", whoami);

    console.log({ user }); // user is fully typed
    return "...";
}

use in server components:

export default async function ServerPage() {
    const user = await whoami();

    console.log({ user }); // user is fully typed
    return "...";
}

The is a very good method, why this is not documented in nextjs or swr docs ? (I did not found it)

2

Answers


  1. I have the same issue, and I use this approach in most parts of the project.

    Do you have any new insights now?

    Login or Signup to reply.
  2. While using server actions directly as SWR fetchers in Next.js isn’t the most common approach, there are valid use cases and alternative strategies to consider:

    Reasons Why SWR Might Not Be Ideal for Server Actions:

    Code Duplication: Using server actions within SWR fetchers can lead to redundant code, as you’d essentially be fetching data twice (once in the server action and again in the SWR fetcher).

    Unnecessary Client-Side Fetching: If the data doesn’t need to be dynamically updated on the client, fetching it on the server side during the initial render (SSR) or static site generation (SSG) can be more efficient.

    This is how you can fetch data :

    import DashboardProperitesMain from '@/components/DashboardProperitesPage';
    import UserCtxLayoutProvider from '@/components/ui/UserCtxLayoutProvider';
    import { PropertiesStatusFilterEnum } from '@/config';
    import { Roles } from '@/schemas';
    import { getMeAction } from '@/server/auth-actions';
    import { redirectBaseOnRole } from '@/server/helper';
    import { UserType } from '@/types';
    
    export default async function DashboardHome({
      searchParams,
    }: {
      searchParams?: {
        page?: string;
        status?: PropertiesStatusFilterEnum;
      };
    }) {
      const currentPage = Number(searchParams?.page) || 1;
      const userData = (await getMeAction())?.success;
      redirectBaseOnRole(userData, [
        Roles.ADMIN,
        Roles.LANDLORD,
        Roles.SUB_USER,
        Roles.AGENT,
      ]); 
      return (
        <UserCtxLayoutProvider userData={userData}>
          <DashboardProperitesMain
            userData={userData as UserType}
            currentPage={currentPage}
            status={searchParams?.status}
          />
        </UserCtxLayoutProvider>
      );
    }
    

    User data:

    'use client';
    import React from 'react';
    import { MeCtxProvider } from '@/hooks/useMe';
    import { UserType } from '@/types';
    
    export default function UserCtxLayoutProvider({
      userData,
      children,
    }: React.PropsWithChildren & {
      userData: UserType | undefined;
    }) {
      return (
        <MeCtxProvider userDetails={userData}>{children}</MeCtxProvider>
      );
    }
    
    

    CTX:

    'use client';
    import { UserType } from '@/types';
    import React from 'react';
    
    type MeContextValueType = {
      userDetails: UserType | undefined;
    } | null;
    
    const MeContext = React.createContext<MeContextValueType>(null);
    
    type Props = {
      children: React.ReactNode;
      userDetails: UserType | undefined;
    };
    
    export const MeCtxProvider = ({ children, userDetails }: Props) => {
      const value: MeContextValueType = {
        userDetails,
      };
      return (
        <MeContext.Provider value={value}>{children}</MeContext.Provider>
      );
    };
    
    const useMe = () => {
      const context = React.useContext(MeContext);
      if (!context) {
        throw new Error(
          'usePropertyDetails must be used within a MeCtxProvider'
        );
      }
      return context.userDetails;
    };
    
    export default useMe;
    
    

    Actions:

    export const getMeAction = async () => {
      try {
        const res = await fetch(`${process.env.BACKEND_BASE}/users/me`, {
          cache: 'no-store',
          headers: { ...getAuthorizationHeader() },
          next: { tags: [getCacheKey('userData')] },
        });
        if (!res.ok) {
          throw new Error('server_error');
        }
        const { data } = await res.json();
        return { success: data as UserType };
      } catch (err) {
        return { error: 'server_error' };
      }
    };
    

    Main competent:

    import React from 'react';
    
    import { Suspense } from 'react';
    import TableWrapper from '@/components/DashboardProperitesPage/TableWrapper';
    import Header from '@/components/DashboardProperitesPage/Header';
    import { DashboardProperitesTableSkelton } from '@/components/ui/DashboardPropertiesTable';
    import { UserType } from '@/types';
    import { PropertiesStatusFilterEnum } from '@/config';
    type Props = {
      currentPage: number;
      userData: UserType | undefined;
      status?: PropertiesStatusFilterEnum;
    };
    export default function DashboardProperitesMain({
      currentPage,
      userData,
      status,
    }: Props) {
      return (
        <section className=' max-w-[1300px] mx-auto '>
          <Header />
          <Suspense
            key={`${currentPage}-${status}`}
            fallback={<DashboardProperitesTableSkelton />}
          >
            <TableWrapper
              status={status}
              userData={userData}
              currentPage={currentPage}
            />
          </Suspense>
        </section>
      );
    }
    

    TableWrapper

    import React from 'react';
    import {
      getAllProperitesAction,
      getMyProperitesAction,
    } from '@/server/property-actions';
    import { DashboardPropertiesTable } from '@/components/ui/DashboardPropertiesTable';
    import { UserType } from '@/types';
    import { Roles } from '@/schemas';
    import { PropertiesStatusFilterEnum } from '@/config';
    type Props = {
      currentPage: number;
      userData: UserType | undefined;
      status?: PropertiesStatusFilterEnum;
    };
    export default async function TableWrapper({
      currentPage,
      userData,
      status,
    }: Props) {
      let properitesData;
    
      if (userData?.role === Roles.ADMIN) {
        properitesData = await getAllProperitesAction(
          currentPage,
          status
        );
      } else {
        properitesData = await getMyProperitesAction(currentPage, status);
      }
    
      return (
        <DashboardPropertiesTable
          data={properitesData.data}
          totalDocs={properitesData.totalDocs}
          currentPage={currentPage}
        />
      );
    }
    

    Action:

    export async function getMyProperitesAction(
      page: number,
      status: PropertiesStatusFilterEnum | undefined
    ) {
      try {
        let baseUrl = new URL(
          `${process.env.BACKEND_BASE}/properties/get-my-properties?page=${page}&limit=10`
        );
        const url = addStatusSearchParamsBasedOnFilterValue(
          baseUrl,
          status
        );
    
        const res: Response = await fetch(url, {
          method: REQUEST_METHODS.GET,
          headers: {
            ...getNodeRequiredRequestHeaders(),
            ...getAuthorizationHeader(),
          },
          next: { tags: [getCacheKey('properites')] },
        });
        if (res.status === 404) {
          throw new Error('404');
        }
        if (!res.ok) {
          throw new Error('unexpected error');
        }
        const data: APIResponse<PropertyType> = await res.json();
        return data;
      } catch (err: any) {
        if (err.message === '404') {
          const data: APIResponse<PropertyType> = {
            data: [],
            pages: 1,
            results: 0,
            totalDocs: 0,
          };
          return data;
        }
        // TODO NEED TO ADD error.tsx to handle this error
        throw err;
      }
    }
    

    This is how you post data to server :

    import React from 'react';
    import Bumping from '@/components/ui/Bumping';
    import Button from '@/components/ui/Button';
    import Input from '@/components/ui/Input';
    import Text from '@/components/ui/Text';
    import ServerError from '@/components/ui/ServerError';
    import { getRandomId } from '@/util';
    
    import {
      subUserFormSchema,
      SubUserFormSchemaType,
    } from '@/schemas/sub-users';
    import { createSubUserSafeAction } from '@/server/sub-users';
    import { useAction } from 'next-safe-action/hooks';
    import { Controller, SubmitHandler, useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    
    export default function Form({
      onSuccess,
    }: {
      onSuccess: () => void;
    }) {
      const {
        execute: createSubUser,
        status: createSubUserStatus,
        result: createSubUserResult,
        reset: resetCreateSubUser,
      } = useAction(createSubUserSafeAction, {
        onSuccess: data => {
          if (data.success) {
            onSuccess();
          }
        },
      });
    
      const {
        handleSubmit,
        control,
        formState: { errors, isValid },
      } = useForm<SubUserFormSchemaType>({
        resolver: zodResolver(subUserFormSchema),
        mode: 'onSubmit',
      });
    
      const onSubmit: SubmitHandler<
        SubUserFormSchemaType
      > = formValues => {
        createSubUser({ ...formValues });
        resetCreateSubUser();
      };
    
      return (
        <Bumping
          isActive={true}
          As={'form'}
          className='max-w-2xl p-10 relative rounded-lg bg-white  mx-auto'
          onSubmit={handleSubmit(onSubmit)}
        >
          <Text
            color='charcoal'
            as='h2'
            className='font-semibold leading-[1.1] pb-6 text-center'
            size='2xl'
            noMargin
          >
            User Information
          </Text>
          <Controller
            name='firstName'
            control={control}
            render={({ field }) => (
              <Input
                errors={errors}
                label='First Name'
                placeholder='Mr. '
                field={field}
                type='text'
                fieldName='firstName'
              />
            )}
          />
          <Controller
            control={control}
            name='lastName'
            render={({ field }) => (
              <Input
                errors={errors}
                field={field}
                label='Last Name'
                placeholder='John'
                type='text'
                fieldName='lastName'
              />
            )}
          />
          <Controller
            control={control}
            name='email'
            render={({ field }) => (
              <Input
                errors={errors}
                label='Email'
                field={field}
                type='text'
                placeholder='[email protected]'
                fieldName='email'
              />
            )}
          />
          <Controller
            name='password'
            control={control}
            render={({ field }) => (
              <Input
                errors={errors}
                placeholder='**********'
                field={field}
                type='password'
                label='Password'
                fieldName='password'
              />
            )}
          />
          <Controller
            control={control}
            name='passwordConfirm'
            render={({ field }) => (
              <Input
                errors={errors}
                field={field}
                fieldName='passwordConfirm'
                label='Confirm Password'
                type='password'
                placeholder='*******'
              />
            )}
          />
          <Button
            type='submit'
            className='w-full rounded-full mt-5'
            size='lg'
            isLoading={createSubUserStatus === 'executing'}
            color={isValid ? 'yellow' : 'borderedBlack'}
          >
            Create
          </Button>
          <ServerError
            key={getRandomId()}
            onHide={resetCreateSubUser}
            error={createSubUserResult?.data?.error}
            className='mb-1'
          />
        </Bumping>
      );
    }
    
    

    Action:

    import { createSafeActionClient } from 'next-safe-action';
    export const action = createSafeActionClient();
    
    import { z } from 'zod';
    export const subUserFormSchema = z
      .object({
        firstName: alphaOnlyStringSchema,
        lastName: alphaOnlyStringSchema,
        email: emailSchema,
        password: z
          .string()
          .min(8, 'Password must be at least 8 characters long'),
        passwordConfirm: z
          .string()
          .min(8, 'Confirm password must be at least 8 characters long'),
      })
      .refine(data => data.password === data.passwordConfirm, {
        message: "Passwords don't match",
        path: ['passwordConfirm'],
      });
    export const createSubUserSafeAction = action(
      subUserFormSchema,
      async body => {
        try {
          const res = await fetch(
            `${process.env.BACKEND_BASE}/users/sub-user`,
            {
              method: REQUEST_METHODS.POST,
              body: JSON.stringify(body),
              headers: {
                ...getNodeRequiredRequestHeaders(),
                ...getJsonTypeHeader(),
                ...getAuthorizationHeader(),
              },
            }
          );
          if (!res.ok) {
            const errorResponse = await getApiError(res);
            throw new Error(errorResponse.error);
          }
          revalidateTag(getCacheKey('subUsers'));
          const subUserData: SubUserType = await res.json();
          return { success: subUserData };
        } catch (err: any) {
          if (err.message.includes('Duplicate (email) found')) {
            return { error: 'Email already exists' };
          }
          return { error: err.message };
        }
      }
    );
    

    NOTE: keep in mind that I haven’t any specific auth check in the server action because for this I’m totally relying on Nest js backend for your case you must check for auth first in server action

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search