diff --git a/src/app/groups/[groupId]/activity/activity-list.tsx b/src/app/groups/[groupId]/activity/activity-list.tsx
index 04717172..7dfe538a 100644
--- a/src/app/groups/[groupId]/activity/activity-list.tsx
+++ b/src/app/groups/[groupId]/activity/activity-list.tsx
@@ -9,6 +9,7 @@ import dayjs, { type Dayjs } from 'dayjs'
 import { useTranslations } from 'next-intl'
 import { forwardRef, useEffect } from 'react'
 import { useInView } from 'react-intersection-observer'
+import { useCurrentGroup } from '../current-group-context'
 
 const PAGE_SIZE = 20
 
@@ -82,11 +83,9 @@ const ActivitiesLoading = forwardRef<HTMLDivElement>((_, ref) => {
 })
 ActivitiesLoading.displayName = 'ActivitiesLoading'
 
-export function ActivityList({ groupId }: { groupId: string }) {
+export function ActivityList() {
   const t = useTranslations('Activity')
-
-  const { data: groupData, isLoading: groupIsLoading } =
-    trpc.groups.get.useQuery({ groupId })
+  const { group, groupId } = useCurrentGroup()
 
   const {
     data: activitiesData,
@@ -105,7 +104,7 @@ export function ActivityList({ groupId }: { groupId: string }) {
     if (inView && hasMore && !isLoading) fetchNextPage()
   }, [fetchNextPage, hasMore, inView, isLoading])
 
-  if (isLoading || !activities || !groupData) return <ActivitiesLoading />
+  if (isLoading || !activities || !group) return <ActivitiesLoading />
 
   const groupedActivitiesByDate = getGroupedActivitiesByDate(activities)
 
@@ -131,7 +130,7 @@ export function ActivityList({ groupId }: { groupId: string }) {
             {groupActivities.map((activity) => {
               const participant =
                 activity.participantId !== null
-                  ? groupData.group.participants.find(
+                  ? group.participants.find(
                       (p) => p.id === activity.participantId,
                     )
                   : undefined
diff --git a/src/app/groups/[groupId]/activity/page.client.tsx b/src/app/groups/[groupId]/activity/page.client.tsx
index 4f3318cd..3090bd3c 100644
--- a/src/app/groups/[groupId]/activity/page.client.tsx
+++ b/src/app/groups/[groupId]/activity/page.client.tsx
@@ -13,7 +13,7 @@ export const metadata: Metadata = {
   title: 'Activity',
 }
 
-export function ActivityPageClient({ groupId }: { groupId: string }) {
+export function ActivityPageClient() {
   const t = useTranslations('Activity')
 
   return (
@@ -24,7 +24,7 @@ export function ActivityPageClient({ groupId }: { groupId: string }) {
           <CardDescription>{t('description')}</CardDescription>
         </CardHeader>
         <CardContent className="flex flex-col space-y-4">
-          <ActivityList groupId={groupId} />
+          <ActivityList />
         </CardContent>
       </Card>
     </>
diff --git a/src/app/groups/[groupId]/activity/page.tsx b/src/app/groups/[groupId]/activity/page.tsx
index 43408214..e80e4983 100644
--- a/src/app/groups/[groupId]/activity/page.tsx
+++ b/src/app/groups/[groupId]/activity/page.tsx
@@ -5,10 +5,6 @@ export const metadata: Metadata = {
   title: 'Activity',
 }
 
-export default async function ActivityPage({
-  params: { groupId },
-}: {
-  params: { groupId: string }
-}) {
-  return <ActivityPageClient groupId={groupId} />
+export default async function ActivityPage() {
+  return <ActivityPageClient />
 }
diff --git a/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx b/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx
index 687f8879..461cfe3f 100644
--- a/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx
+++ b/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx
@@ -13,15 +13,12 @@ import { Skeleton } from '@/components/ui/skeleton'
 import { trpc } from '@/trpc/client'
 import { useTranslations } from 'next-intl'
 import { Fragment, useEffect } from 'react'
+import { match } from 'ts-pattern'
+import { useCurrentGroup } from '../current-group-context'
 
-export default function BalancesAndReimbursements({
-  groupId,
-}: {
-  groupId: string
-}) {
+export default function BalancesAndReimbursements() {
   const utils = trpc.useUtils()
-  const { data: groupData, isLoading: groupIsLoading } =
-    trpc.groups.get.useQuery({ groupId })
+  const { groupId, group } = useCurrentGroup()
   const { data: balancesData, isLoading: balancesAreLoading } =
     trpc.groups.balances.list.useQuery({
       groupId,
@@ -34,8 +31,7 @@ export default function BalancesAndReimbursements({
     utils.groups.balances.invalidate()
   }, [utils])
 
-  const isLoading =
-    balancesAreLoading || !balancesData || groupIsLoading || !groupData?.group
+  const isLoading = balancesAreLoading || !balancesData || !group
 
   return (
     <>
@@ -46,14 +42,12 @@ export default function BalancesAndReimbursements({
         </CardHeader>
         <CardContent>
           {isLoading ? (
-            <BalancesLoading
-              participantCount={groupData?.group.participants.length}
-            />
+            <BalancesLoading participantCount={group?.participants.length} />
           ) : (
             <BalancesList
               balances={balancesData.balances}
-              participants={groupData.group.participants}
-              currency={groupData.group.currency}
+              participants={group?.participants}
+              currency={group?.currency}
             />
           )}
         </CardContent>
@@ -66,14 +60,14 @@ export default function BalancesAndReimbursements({
         <CardContent>
           {isLoading ? (
             <ReimbursementsLoading
-              participantCount={groupData?.group.participants.length}
+              participantCount={group?.participants.length}
             />
           ) : (
             <ReimbursementList
               reimbursements={balancesData.reimbursements}
-              participants={groupData.group.participants}
-              currency={groupData.group.currency}
-              groupId={groupData.group.id}
+              participants={group?.participants}
+              currency={group?.currency}
+              groupId={groupId}
             />
           )}
         </CardContent>
@@ -109,6 +103,12 @@ const BalancesLoading = ({
 }: {
   participantCount?: number
 }) => {
+  const barWidth = (index: number) =>
+    match(index % 3)
+      .with(0, () => 'w-1/3')
+      .with(1, () => 'w-2/3')
+      .otherwise(() => 'w-full')
+
   return (
     <div className="grid grid-cols-2 py-1 gap-y-2">
       {Array(participantCount)
@@ -120,17 +120,13 @@ const BalancesLoading = ({
                 <Skeleton className="h-3 w-16" />
               </div>
               <div className="self-start">
-                <Skeleton
-                  className={`h-7 w-${(index % 3) + 1}/3 rounded-l-none`}
-                />
+                <Skeleton className={`h-7 ${barWidth(index)} rounded-l-none`} />
               </div>
             </Fragment>
           ) : (
             <Fragment key={index}>
               <div className="flex items-center justify-end">
-                <Skeleton
-                  className={`h-7 w-${(index % 3) + 1}/3 rounded-r-none`}
-                />
+                <Skeleton className={`h-7 ${barWidth(index)} rounded-r-none`} />
               </div>
               <div className="flex items-center pl-2">
                 <Skeleton className="h-3 w-16" />
diff --git a/src/app/groups/[groupId]/balances/page.tsx b/src/app/groups/[groupId]/balances/page.tsx
index a9e7c813..456f40b2 100644
--- a/src/app/groups/[groupId]/balances/page.tsx
+++ b/src/app/groups/[groupId]/balances/page.tsx
@@ -1,19 +1,10 @@
-import { cached } from '@/app/cached-functions'
 import BalancesAndReimbursements from '@/app/groups/[groupId]/balances/balances-and-reimbursements'
 import { Metadata } from 'next'
-import { notFound } from 'next/navigation'
 
 export const metadata: Metadata = {
   title: 'Balances',
 }
 
-export default async function GroupPage({
-  params: { groupId },
-}: {
-  params: { groupId: string }
-}) {
-  const group = await cached.getGroup(groupId)
-  if (!group) notFound()
-
-  return <BalancesAndReimbursements groupId={groupId} />
+export default async function GroupPage() {
+  return <BalancesAndReimbursements />
 }
diff --git a/src/app/groups/[groupId]/current-group-context.tsx b/src/app/groups/[groupId]/current-group-context.tsx
new file mode 100644
index 00000000..b2a47991
--- /dev/null
+++ b/src/app/groups/[groupId]/current-group-context.tsx
@@ -0,0 +1,30 @@
+import { AppRouterOutput } from '@/trpc/routers/_app'
+import { PropsWithChildren, createContext, useContext } from 'react'
+
+type Group = NonNullable<AppRouterOutput['groups']['get']['group']>
+
+type GroupContext =
+  | { isLoading: false; groupId: string; group: Group }
+  | { isLoading: true; groupId: string; group: undefined }
+
+const CurrentGroupContext = createContext<GroupContext | null>(null)
+
+export const useCurrentGroup = () => {
+  const context = useContext(CurrentGroupContext)
+  if (!context)
+    throw new Error(
+      'Missing context. Should be called inside a CurrentGroupProvider.',
+    )
+  return context
+}
+
+export const CurrentGroupProvider = ({
+  children,
+  ...props
+}: PropsWithChildren<GroupContext>) => {
+  return (
+    <CurrentGroupContext.Provider value={props}>
+      {children}
+    </CurrentGroupContext.Provider>
+  )
+}
diff --git a/src/app/groups/[groupId]/edit/edit-group.tsx b/src/app/groups/[groupId]/edit/edit-group.tsx
index 52cb4a1b..9189c895 100644
--- a/src/app/groups/[groupId]/edit/edit-group.tsx
+++ b/src/app/groups/[groupId]/edit/edit-group.tsx
@@ -2,9 +2,11 @@
 
 import { GroupForm } from '@/components/group-form'
 import { trpc } from '@/trpc/client'
+import { useCurrentGroup } from '../current-group-context'
 
-export const EditGroup = ({ groupId }: { groupId: string }) => {
-  const { data, isLoading } = trpc.groups.get.useQuery({ groupId })
+export const EditGroup = () => {
+  const { groupId } = useCurrentGroup()
+  const { data, isLoading } = trpc.groups.getDetails.useQuery({ groupId })
   const { mutateAsync } = trpc.groups.update.useMutation()
   const utils = trpc.useUtils()
 
diff --git a/src/app/groups/[groupId]/edit/page.tsx b/src/app/groups/[groupId]/edit/page.tsx
index 66b83ed4..aac1d928 100644
--- a/src/app/groups/[groupId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/edit/page.tsx
@@ -5,10 +5,6 @@ export const metadata: Metadata = {
   title: 'Settings',
 }
 
-export default async function EditGroupPage({
-  params: { groupId },
-}: {
-  params: { groupId: string }
-}) {
-  return <EditGroup groupId={groupId} />
+export default async function EditGroupPage() {
+  return <EditGroup />
 }
diff --git a/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx b/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx
index cd8edfd9..34c42dee 100644
--- a/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx
+++ b/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx
@@ -34,14 +34,11 @@ import { getImageData, usePresignedUpload } from 'next-s3-upload'
 import Image from 'next/image'
 import { useRouter } from 'next/navigation'
 import { PropsWithChildren, ReactNode, useState } from 'react'
+import { useCurrentGroup } from '../current-group-context'
 
 const MAX_FILE_SIZE = 5 * 1024 ** 2
 
-export function CreateFromReceiptButton({ groupId }: { groupId: string }) {
-  return <CreateFromReceiptButton_ groupId={groupId} />
-}
-
-function CreateFromReceiptButton_({ groupId }: { groupId: string }) {
+export function CreateFromReceiptButton() {
   const t = useTranslations('CreateFromReceipt')
   const isDesktop = useMediaQuery('(min-width: 640px)')
 
@@ -70,15 +67,14 @@ function CreateFromReceiptButton_({ groupId }: { groupId: string }) {
       }
       description={<>{t('Dialog.description')}</>}
     >
-      <ReceiptDialogContent groupId={groupId} />
+      <ReceiptDialogContent />
     </DialogOrDrawer>
   )
 }
 
-function ReceiptDialogContent({ groupId }: { groupId: string }) {
-  const { data: groupData } = trpc.groups.get.useQuery({ groupId })
+function ReceiptDialogContent() {
+  const { group } = useCurrentGroup()
   const { data: categoriesData } = trpc.categories.list.useQuery()
-  const group = groupData?.group
   const categories = categoriesData?.categories
 
   const locale = useLocale()
diff --git a/src/app/groups/[groupId]/expenses/expense-form.tsx b/src/app/groups/[groupId]/expenses/expense-form.tsx
index 08f44363..b0bb6789 100644
--- a/src/app/groups/[groupId]/expenses/expense-form.tsx
+++ b/src/app/groups/[groupId]/expenses/expense-form.tsx
@@ -64,7 +64,7 @@ const enforceCurrencyPattern = (value: string) =>
     .replace(/[^-\d.]/g, '') // remove all non-numeric characters
 
 const getDefaultSplittingOptions = (
-  group: AppRouterOutput['groups']['get']['group'],
+  group: NonNullable<AppRouterOutput['groups']['get']['group']>,
 ) => {
   const defaultValue = {
     splitMode: 'EVENLY' as const,
@@ -145,7 +145,7 @@ export function ExpenseForm({
   onDelete,
   runtimeFeatureFlags,
 }: {
-  group: AppRouterOutput['groups']['get']['group']
+  group: NonNullable<AppRouterOutput['groups']['get']['group']>
   categories: AppRouterOutput['categories']['list']['categories']
   expense?: AppRouterOutput['groups']['expenses']['get']['expense']
   onSubmit: (value: ExpenseFormValues, participantId?: string) => Promise<void>
@@ -250,7 +250,6 @@ export function ExpenseForm({
   >(new Set())
 
   const sExpense = isIncome ? 'Income' : 'Expense'
-  const sPaid = isIncome ? 'received' : 'paid'
 
   useEffect(() => {
     setManuallyEditedParticipants(new Set())
diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx
index 66ef409f..bd498a1e 100644
--- a/src/app/groups/[groupId]/expenses/expense-list.tsx
+++ b/src/app/groups/[groupId]/expenses/expense-list.tsx
@@ -11,6 +11,7 @@ import Link from 'next/link'
 import { forwardRef, useEffect, useMemo, useState } from 'react'
 import { useInView } from 'react-intersection-observer'
 import { useDebounce } from 'use-debounce'
+import { useCurrentGroup } from '../current-group-context'
 
 const PAGE_SIZE = 20
 
@@ -56,12 +57,12 @@ function getGroupedExpensesByDate(expenses: ExpensesType) {
   }, {})
 }
 
-export function ExpenseList({ groupId }: { groupId: string }) {
-  const { data: groupData } = trpc.groups.get.useQuery({ groupId })
+export function ExpenseList() {
+  const { groupId, group } = useCurrentGroup()
   const [searchText, setSearchText] = useState('')
   const [debouncedSearchText] = useDebounce(searchText, 300)
 
-  const participants = groupData?.group.participants
+  const participants = group?.participants
 
   useEffect(() => {
     if (!participants) return
@@ -103,6 +104,7 @@ const ExpenseListForSearch = ({
   searchText: string
 }) => {
   const utils = trpc.useUtils()
+  const { group } = useCurrentGroup()
 
   useEffect(() => {
     // Until we use tRPC more widely and can invalidate the cache on expense
@@ -124,11 +126,7 @@ const ExpenseListForSearch = ({
   const expenses = data?.pages.flatMap((page) => page.expenses)
   const hasMore = data?.pages.at(-1)?.hasMore ?? false
 
-  const { data: groupData, isLoading: groupIsLoading } =
-    trpc.groups.get.useQuery({ groupId })
-
-  const isLoading =
-    expensesAreLoading || !expenses || groupIsLoading || !groupData
+  const isLoading = expensesAreLoading || !expenses || !group
 
   useEffect(() => {
     if (inView && hasMore && !isLoading) fetchNextPage()
@@ -172,7 +170,7 @@ const ExpenseListForSearch = ({
               <ExpenseCard
                 key={expense.id}
                 expense={expense}
-                currency={groupData.group.currency}
+                currency={group.currency}
                 groupId={groupId}
               />
             ))}
diff --git a/src/app/groups/[groupId]/expenses/page.client.tsx b/src/app/groups/[groupId]/expenses/page.client.tsx
index 94827b12..2a9888e3 100644
--- a/src/app/groups/[groupId]/expenses/page.client.tsx
+++ b/src/app/groups/[groupId]/expenses/page.client.tsx
@@ -15,6 +15,7 @@ import { Download, Plus } from 'lucide-react'
 import { Metadata } from 'next'
 import { useTranslations } from 'next-intl'
 import Link from 'next/link'
+import { useCurrentGroup } from '../current-group-context'
 
 export const revalidate = 3600
 
@@ -23,13 +24,12 @@ export const metadata: Metadata = {
 }
 
 export default function GroupExpensesPageClient({
-  groupId,
   enableReceiptExtract,
 }: {
-  groupId: string
   enableReceiptExtract: boolean
 }) {
   const t = useTranslations('Expenses')
+  const { groupId } = useCurrentGroup()
 
   return (
     <>
@@ -50,9 +50,7 @@ export default function GroupExpensesPageClient({
                 <Download className="w-4 h-4" />
               </Link>
             </Button>
-            {enableReceiptExtract && (
-              <CreateFromReceiptButton groupId={groupId} />
-            )}
+            {enableReceiptExtract && <CreateFromReceiptButton />}
             <Button asChild size="icon">
               <Link
                 href={`/groups/${groupId}/expenses/create`}
@@ -65,7 +63,7 @@ export default function GroupExpensesPageClient({
         </div>
 
         <CardContent className="p-0 pt-2 pb-4 sm:pb-6 flex flex-col gap-4 relative">
-          <ExpenseList groupId={groupId} />
+          <ExpenseList />
         </CardContent>
       </Card>
 
diff --git a/src/app/groups/[groupId]/expenses/page.tsx b/src/app/groups/[groupId]/expenses/page.tsx
index 0d0f9359..19f2c7f8 100644
--- a/src/app/groups/[groupId]/expenses/page.tsx
+++ b/src/app/groups/[groupId]/expenses/page.tsx
@@ -8,14 +8,9 @@ export const metadata: Metadata = {
   title: 'Expenses',
 }
 
-export default async function GroupExpensesPage({
-  params: { groupId },
-}: {
-  params: { groupId: string }
-}) {
+export default async function GroupExpensesPage() {
   return (
     <GroupExpensesPageClient
-      groupId={groupId}
       enableReceiptExtract={env.NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT}
     />
   )
diff --git a/src/app/groups/[groupId]/group-header.tsx b/src/app/groups/[groupId]/group-header.tsx
index 154ba7f5..49c741c1 100644
--- a/src/app/groups/[groupId]/group-header.tsx
+++ b/src/app/groups/[groupId]/group-header.tsx
@@ -3,27 +3,27 @@
 import { GroupTabs } from '@/app/groups/[groupId]/group-tabs'
 import { ShareButton } from '@/app/groups/[groupId]/share-button'
 import { Skeleton } from '@/components/ui/skeleton'
-import { trpc } from '@/trpc/client'
 import Link from 'next/link'
+import { useCurrentGroup } from './current-group-context'
 
-export const GroupHeader = ({ groupId }: { groupId: string }) => {
-  const { data, isLoading } = trpc.groups.get.useQuery({ groupId })
+export const GroupHeader = () => {
+  const { isLoading, groupId, group } = useCurrentGroup()
 
   return (
     <div className="flex flex-col justify-between gap-3">
       <h1 className="font-bold text-2xl">
         <Link href={`/groups/${groupId}`}>
-          {isLoading || !data ? (
+          {isLoading ? (
             <Skeleton className="mt-1.5 mb-1.5 h-5 w-32" />
           ) : (
-            <div className="flex">{data.group.name}</div>
+            <div className="flex">{group.name}</div>
           )}
         </Link>
       </h1>
 
       <div className="flex gap-2 justify-between">
         <GroupTabs groupId={groupId} />
-        {data?.group && <ShareButton group={data.group} />}
+        {group && <ShareButton group={group} />}
       </div>
     </div>
   )
diff --git a/src/app/groups/[groupId]/information/group-information.tsx b/src/app/groups/[groupId]/information/group-information.tsx
index fd3d88a2..73cdadb9 100644
--- a/src/app/groups/[groupId]/information/group-information.tsx
+++ b/src/app/groups/[groupId]/information/group-information.tsx
@@ -9,14 +9,14 @@ import {
   CardTitle,
 } from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
-import { trpc } from '@/trpc/client'
 import { Pencil } from 'lucide-react'
 import { useTranslations } from 'next-intl'
 import Link from 'next/link'
+import { useCurrentGroup } from '../current-group-context'
 
 export default function GroupInformation({ groupId }: { groupId: string }) {
   const t = useTranslations('Information')
-  const { data, isLoading } = trpc.groups.get.useQuery({ groupId })
+  const { isLoading, group } = useCurrentGroup()
 
   return (
     <>
@@ -35,13 +35,13 @@ export default function GroupInformation({ groupId }: { groupId: string }) {
           </CardDescription>
         </CardHeader>
         <CardContent className="prose prose-sm sm:prose-base max-w-full whitespace-break-spaces">
-          {isLoading || !data ? (
+          {isLoading ? (
             <div className="py-1 flex flex-col gap-2">
               <Skeleton className="h-3 w-3/4" />
               <Skeleton className="h-3 w-1/2" />
             </div>
-          ) : data.group.information ? (
-            <p className="text-foreground">{data.group.information}</p>
+          ) : group.information ? (
+            <p className="text-foreground">{group.information}</p>
           ) : (
             <p className="text-muted-foreground text-sm">{t('empty')}</p>
           )}
diff --git a/src/app/groups/[groupId]/layout.client.tsx b/src/app/groups/[groupId]/layout.client.tsx
new file mode 100644
index 00000000..e04f222b
--- /dev/null
+++ b/src/app/groups/[groupId]/layout.client.tsx
@@ -0,0 +1,49 @@
+'use client'
+
+import { useToast } from '@/components/ui/use-toast'
+import { trpc } from '@/trpc/client'
+import { useTranslations } from 'next-intl'
+import { PropsWithChildren, useEffect } from 'react'
+import { CurrentGroupProvider } from './current-group-context'
+import { GroupHeader } from './group-header'
+import { SaveGroupLocally } from './save-recent-group'
+
+export function GroupLayoutClient({
+  groupId,
+  children,
+}: PropsWithChildren<{ groupId: string }>) {
+  const { data, isLoading } = trpc.groups.get.useQuery({ groupId })
+  const t = useTranslations('Groups.NotFound')
+  const { toast } = useToast()
+
+  useEffect(() => {
+    if (data && !data.group) {
+      toast({
+        description: t('text'),
+        variant: 'destructive',
+      })
+    }
+  }, [data])
+
+  const props =
+    isLoading || !data?.group
+      ? { isLoading: true as const, groupId, group: undefined }
+      : { isLoading: false as const, groupId, group: data.group }
+
+  if (isLoading) {
+    return (
+      <CurrentGroupProvider {...props}>
+        <GroupHeader />
+        {children}
+      </CurrentGroupProvider>
+    )
+  }
+
+  return (
+    <CurrentGroupProvider {...props}>
+      <GroupHeader />
+      {children}
+      <SaveGroupLocally />
+    </CurrentGroupProvider>
+  )
+}
diff --git a/src/app/groups/[groupId]/layout.tsx b/src/app/groups/[groupId]/layout.tsx
index f8206d8f..e9afd6c9 100644
--- a/src/app/groups/[groupId]/layout.tsx
+++ b/src/app/groups/[groupId]/layout.tsx
@@ -1,9 +1,7 @@
 import { cached } from '@/app/cached-functions'
-import { GroupHeader } from '@/app/groups/[groupId]/group-header'
-import { SaveGroupLocally } from '@/app/groups/[groupId]/save-recent-group'
 import { Metadata } from 'next'
-import { notFound } from 'next/navigation'
 import { PropsWithChildren } from 'react'
+import { GroupLayoutClient } from './layout.client'
 
 type Props = {
   params: {
@@ -24,20 +22,9 @@ export async function generateMetadata({
   }
 }
 
-export default async function GroupLayout({
+export default function GroupLayout({
   children,
   params: { groupId },
 }: PropsWithChildren<Props>) {
-  const group = await cached.getGroup(groupId)
-  if (!group) notFound()
-
-  return (
-    <>
-      <GroupHeader groupId={groupId} />
-
-      {children}
-
-      <SaveGroupLocally group={{ id: group.id, name: group.name }} />
-    </>
-  )
+  return <GroupLayoutClient groupId={groupId}>{children}</GroupLayoutClient>
 }
diff --git a/src/app/groups/[groupId]/save-recent-group.tsx b/src/app/groups/[groupId]/save-recent-group.tsx
index 084681aa..27aa3e96 100644
--- a/src/app/groups/[groupId]/save-recent-group.tsx
+++ b/src/app/groups/[groupId]/save-recent-group.tsx
@@ -1,17 +1,13 @@
 'use client'
-import {
-  RecentGroup,
-  saveRecentGroup,
-} from '@/app/groups/recent-groups-helpers'
+import { saveRecentGroup } from '@/app/groups/recent-groups-helpers'
 import { useEffect } from 'react'
+import { useCurrentGroup } from './current-group-context'
 
-type Props = {
-  group: RecentGroup
-}
+export function SaveGroupLocally() {
+  const { group } = useCurrentGroup()
 
-export function SaveGroupLocally({ group }: Props) {
   useEffect(() => {
-    saveRecentGroup(group)
+    if (group) saveRecentGroup({ id: group.id, name: group.name })
   }, [group])
 
   return null
diff --git a/src/app/groups/[groupId]/stats/page.client.tsx b/src/app/groups/[groupId]/stats/page.client.tsx
index 17da5597..9256ecc1 100644
--- a/src/app/groups/[groupId]/stats/page.client.tsx
+++ b/src/app/groups/[groupId]/stats/page.client.tsx
@@ -8,7 +8,7 @@ import {
 } from '@/components/ui/card'
 import { useTranslations } from 'next-intl'
 
-export function TotalsPageClient({ groupId }: { groupId: string }) {
+export function TotalsPageClient() {
   const t = useTranslations('Stats')
 
   return (
@@ -19,7 +19,7 @@ export function TotalsPageClient({ groupId }: { groupId: string }) {
           <CardDescription>{t('Totals.description')}</CardDescription>
         </CardHeader>
         <CardContent className="flex flex-col space-y-4">
-          <Totals groupId={groupId} />
+          <Totals />
         </CardContent>
       </Card>
     </>
diff --git a/src/app/groups/[groupId]/stats/page.tsx b/src/app/groups/[groupId]/stats/page.tsx
index 38c1d009..5bafb677 100644
--- a/src/app/groups/[groupId]/stats/page.tsx
+++ b/src/app/groups/[groupId]/stats/page.tsx
@@ -5,10 +5,6 @@ export const metadata: Metadata = {
   title: 'Totals',
 }
 
-export default async function TotalsPage({
-  params: { groupId },
-}: {
-  params: { groupId: string }
-}) {
-  return <TotalsPageClient groupId={groupId} />
+export default async function TotalsPage() {
+  return <TotalsPageClient />
 }
diff --git a/src/app/groups/[groupId]/stats/totals.tsx b/src/app/groups/[groupId]/stats/totals.tsx
index 911be839..d1ea4e3e 100644
--- a/src/app/groups/[groupId]/stats/totals.tsx
+++ b/src/app/groups/[groupId]/stats/totals.tsx
@@ -5,16 +5,17 @@ import { TotalsYourSpendings } from '@/app/groups/[groupId]/stats/totals-your-sp
 import { Skeleton } from '@/components/ui/skeleton'
 import { useActiveUser } from '@/lib/hooks'
 import { trpc } from '@/trpc/client'
+import { useCurrentGroup } from '../current-group-context'
 
-export function Totals({ groupId }: { groupId: string }) {
+export function Totals() {
+  const { groupId, group } = useCurrentGroup()
   const activeUser = useActiveUser(groupId)
 
   const participantId =
     activeUser && activeUser !== 'None' ? activeUser : undefined
   const { data } = trpc.groups.stats.get.useQuery({ groupId, participantId })
-  const { data: groupData } = trpc.groups.get.useQuery({ groupId })
 
-  if (!data || !groupData)
+  if (!data || !group)
     return (
       <div className="flex flex-col gap-7">
         {[0, 1, 2].map((index) => (
@@ -31,7 +32,6 @@ export function Totals({ groupId }: { groupId: string }) {
     totalParticipantShare,
     totalParticipantSpendings,
   } = data
-  const { group } = groupData
 
   return (
     <>
diff --git a/src/app/groups/actions.ts b/src/app/groups/actions.ts
deleted file mode 100644
index 9e8e6050..00000000
--- a/src/app/groups/actions.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-'use server'
-import { getGroups } from '@/lib/api'
-
-export async function getGroupsAction(groupIds: string[]) {
-  'use server'
-  return getGroups(groupIds)
-}
diff --git a/src/app/groups/add-group-by-url-button-actions.ts b/src/app/groups/add-group-by-url-button-actions.ts
deleted file mode 100644
index b6c51474..00000000
--- a/src/app/groups/add-group-by-url-button-actions.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-'use server'
-
-import { getGroup } from '@/lib/api'
-
-export async function getGroupInfoAction(groupId: string) {
-  'use server'
-  return getGroup(groupId)
-}
diff --git a/src/app/groups/add-group-by-url-button.tsx b/src/app/groups/add-group-by-url-button.tsx
index a4053d71..da76ca43 100644
--- a/src/app/groups/add-group-by-url-button.tsx
+++ b/src/app/groups/add-group-by-url-button.tsx
@@ -1,4 +1,3 @@
-import { getGroupInfoAction } from '@/app/groups/add-group-by-url-button-actions'
 import { saveRecentGroup } from '@/app/groups/recent-groups-helpers'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
@@ -8,6 +7,7 @@ import {
   PopoverTrigger,
 } from '@/components/ui/popover'
 import { useMediaQuery } from '@/lib/hooks'
+import { trpc } from '@/trpc/client'
 import { Loader2, Plus } from 'lucide-react'
 import { useTranslations } from 'next-intl'
 import { useState } from 'react'
@@ -23,14 +23,12 @@ export function AddGroupByUrlButton({ reload }: Props) {
   const [error, setError] = useState(false)
   const [open, setOpen] = useState(false)
   const [pending, setPending] = useState(false)
+  const utils = trpc.useUtils()
 
   return (
     <Popover open={open} onOpenChange={setOpen}>
       <PopoverTrigger asChild>
-        <Button variant="secondary">
-          {/* <Plus className="w-4 h-4 mr-2" /> */}
-          {t('button')}
-        </Button>
+        <Button variant="secondary">{t('button')}</Button>
       </PopoverTrigger>
       <PopoverContent
         align={isDesktop ? 'end' : 'start'}
@@ -47,15 +45,17 @@ export function AddGroupByUrlButton({ reload }: Props) {
                 new RegExp(`${window.location.origin}/groups/([^/]+)`),
               ) ?? []
             setPending(true)
-            const group = groupId ? await getGroupInfoAction(groupId) : null
-            setPending(false)
-            if (!group) {
-              setError(true)
-            } else {
+            const { group } = await utils.groups.get.fetch({
+              groupId: groupId,
+            })
+            if (group) {
               saveRecentGroup({ id: group.id, name: group.name })
               reload()
               setUrl('')
               setOpen(false)
+            } else {
+              setError(true)
+              setPending(false)
             }
           }}
         >
diff --git a/src/app/groups/recent-group-list-card.tsx b/src/app/groups/recent-group-list-card.tsx
index 30692102..8984e783 100644
--- a/src/app/groups/recent-group-list-card.tsx
+++ b/src/app/groups/recent-group-list-card.tsx
@@ -1,12 +1,7 @@
-'use client'
-import { RecentGroupsState } from '@/app/groups/recent-group-list'
 import {
   RecentGroup,
   archiveGroup,
   deleteRecentGroup,
-  getArchivedGroups,
-  getStarredGroups,
-  saveRecentGroup,
   starGroup,
   unarchiveGroup,
   unstarGroup,
@@ -19,46 +14,32 @@ import {
   DropdownMenuTrigger,
 } from '@/components/ui/dropdown-menu'
 import { Skeleton } from '@/components/ui/skeleton'
-import { ToastAction } from '@/components/ui/toast'
 import { useToast } from '@/components/ui/use-toast'
+import { AppRouterOutput } from '@/trpc/routers/_app'
 import { StarFilledIcon } from '@radix-ui/react-icons'
 import { Calendar, MoreHorizontal, Star, Users } from 'lucide-react'
 import { useLocale, useTranslations } from 'next-intl'
 import Link from 'next/link'
 import { useRouter } from 'next/navigation'
-import { SetStateAction } from 'react'
 
 export function RecentGroupListCard({
   group,
-  state,
-  setState,
+  groupDetail,
+  isStarred,
+  isArchived,
+  refreshGroupsFromStorage,
 }: {
   group: RecentGroup
-  state: RecentGroupsState
-  setState: (state: SetStateAction<RecentGroupsState>) => void
+  groupDetail?: AppRouterOutput['groups']['list']['groups'][number]
+  isStarred: boolean
+  isArchived: boolean
+  refreshGroupsFromStorage: () => void
 }) {
   const router = useRouter()
   const locale = useLocale()
   const toast = useToast()
   const t = useTranslations('Groups')
 
-  const details =
-    state.status === 'complete'
-      ? state.groupsDetails.find((d) => d.id === group.id)
-      : null
-
-  if (state.status === 'pending') return null
-
-  const refreshGroupsFromStorage = () =>
-    setState({
-      ...state,
-      starredGroups: getStarredGroups(),
-      archivedGroups: getArchivedGroups(),
-    })
-
-  const isStarred = state.starredGroups.includes(group.id)
-  const isArchived = state.archivedGroups.includes(group.id)
-
   return (
     <li key={group.id}>
       <Button
@@ -116,27 +97,11 @@ export function RecentGroupListCard({
                       onClick={(event) => {
                         event.stopPropagation()
                         deleteRecentGroup(group)
-                        setState({
-                          ...state,
-                          groups: state.groups.filter((g) => g.id !== group.id),
-                        })
+                        refreshGroupsFromStorage()
+
                         toast.toast({
                           title: t('RecentRemovedToast.title'),
                           description: t('RecentRemovedToast.description'),
-                          action: (
-                            <ToastAction
-                              altText={t('RecentRemovedToast.undoAlt')}
-                              onClick={() => {
-                                saveRecentGroup(group)
-                                setState({
-                                  ...state,
-                                  groups: state.groups,
-                                })
-                              }}
-                            >
-                              {t('RecentRemovedToast.undo')}
-                            </ToastAction>
-                          ),
                         })
                       }}
                     >
@@ -161,18 +126,21 @@ export function RecentGroupListCard({
               </span>
             </div>
             <div className="text-muted-foreground font-normal text-xs">
-              {details ? (
+              {groupDetail ? (
                 <div className="w-full flex items-center justify-between">
                   <div className="flex items-center">
                     <Users className="w-3 h-3 inline mr-1" />
-                    <span>{details._count.participants}</span>
+                    <span>{groupDetail._count.participants}</span>
                   </div>
                   <div className="flex items-center">
                     <Calendar className="w-3 h-3 inline mx-1" />
                     <span>
-                      {new Date(details.createdAt).toLocaleDateString(locale, {
-                        dateStyle: 'medium',
-                      })}
+                      {new Date(groupDetail.createdAt).toLocaleDateString(
+                        locale,
+                        {
+                          dateStyle: 'medium',
+                        },
+                      )}
                     </span>
                   </div>
                 </div>
diff --git a/src/app/groups/recent-group-list.tsx b/src/app/groups/recent-group-list.tsx
index ad01d283..3d6465e6 100644
--- a/src/app/groups/recent-group-list.tsx
+++ b/src/app/groups/recent-group-list.tsx
@@ -1,5 +1,4 @@
 'use client'
-import { getGroupsAction } from '@/app/groups/actions'
 import { AddGroupByUrlButton } from '@/app/groups/add-group-by-url-button'
 import {
   RecentGroups,
@@ -9,10 +8,12 @@ import {
 } from '@/app/groups/recent-groups-helpers'
 import { Button } from '@/components/ui/button'
 import { getGroups } from '@/lib/api'
+import { trpc } from '@/trpc/client'
+import { AppRouterOutput } from '@/trpc/routers/_app'
 import { Loader2 } from 'lucide-react'
 import { useTranslations } from 'next-intl'
 import Link from 'next/link'
-import { PropsWithChildren, SetStateAction, useEffect, useState } from 'react'
+import { PropsWithChildren, useEffect, useState } from 'react'
 import { RecentGroupListCard } from './recent-group-list-card'
 
 export type RecentGroupsState =
@@ -31,16 +32,22 @@ export type RecentGroupsState =
       archivedGroups: string[]
     }
 
-function sortGroups(
-  state: RecentGroupsState & { status: 'complete' | 'partial' },
-) {
+function sortGroups({
+  groups,
+  starredGroups,
+  archivedGroups,
+}: {
+  groups: RecentGroups
+  starredGroups: string[]
+  archivedGroups: string[]
+}) {
   const starredGroupInfo = []
   const groupInfo = []
   const archivedGroupInfo = []
-  for (const group of state.groups) {
-    if (state.starredGroups.includes(group.id)) {
+  for (const group of groups) {
+    if (starredGroups.includes(group.id)) {
       starredGroupInfo.push(group)
-    } else if (state.archivedGroups.includes(group.id)) {
+    } else if (archivedGroups.includes(group.id)) {
       archivedGroupInfo.push(group)
     } else {
       groupInfo.push(group)
@@ -54,7 +61,6 @@ function sortGroups(
 }
 
 export function RecentGroupList() {
-  const t = useTranslations('Groups')
   const [state, setState] = useState<RecentGroupsState>({ status: 'pending' })
 
   function loadGroups() {
@@ -67,24 +73,43 @@ export function RecentGroupList() {
       starredGroups,
       archivedGroups,
     })
-    getGroupsAction(groupsInStorage.map((g) => g.id)).then((groupsDetails) => {
-      setState({
-        status: 'complete',
-        groups: groupsInStorage,
-        groupsDetails,
-        starredGroups,
-        archivedGroups,
-      })
-    })
   }
 
   useEffect(() => {
     loadGroups()
   }, [])
 
-  if (state.status === 'pending') {
+  if (state.status === 'pending') return null
+
+  return (
+    <RecentGroupList_
+      groups={state.groups}
+      starredGroups={state.starredGroups}
+      archivedGroups={state.archivedGroups}
+      refreshGroupsFromStorage={() => loadGroups()}
+    />
+  )
+}
+
+function RecentGroupList_({
+  groups,
+  starredGroups,
+  archivedGroups,
+  refreshGroupsFromStorage,
+}: {
+  groups: RecentGroups
+  starredGroups: string[]
+  archivedGroups: string[]
+  refreshGroupsFromStorage: () => void
+}) {
+  const t = useTranslations('Groups')
+  const { data, isLoading } = trpc.groups.list.useQuery({
+    groupIds: groups.map((group) => group.id),
+  })
+
+  if (isLoading || !data) {
     return (
-      <GroupsPage reload={loadGroups}>
+      <GroupsPage reload={refreshGroupsFromStorage}>
         <p>
           <Loader2 className="w-4 m-4 mr-2 inline animate-spin" />{' '}
           {t('loadingRecent')}
@@ -93,9 +118,9 @@ export function RecentGroupList() {
     )
   }
 
-  if (state.groups.length === 0) {
+  if (data.groups.length === 0) {
     return (
-      <GroupsPage reload={loadGroups}>
+      <GroupsPage reload={refreshGroupsFromStorage}>
         <div className="text-sm space-y-2">
           <p>{t('NoRecent.description')}</p>
           <p>
@@ -109,17 +134,23 @@ export function RecentGroupList() {
     )
   }
 
-  const { starredGroupInfo, groupInfo, archivedGroupInfo } = sortGroups(state)
+  const { starredGroupInfo, groupInfo, archivedGroupInfo } = sortGroups({
+    groups,
+    starredGroups,
+    archivedGroups,
+  })
 
   return (
-    <GroupsPage reload={loadGroups}>
+    <GroupsPage reload={refreshGroupsFromStorage}>
       {starredGroupInfo.length > 0 && (
         <>
           <h2 className="mb-2">{t('starred')}</h2>
           <GroupList
             groups={starredGroupInfo}
-            state={state}
-            setState={setState}
+            groupDetails={data.groups}
+            archivedGroups={archivedGroups}
+            starredGroups={starredGroups}
+            refreshGroupsFromStorage={refreshGroupsFromStorage}
           />
         </>
       )}
@@ -127,7 +158,13 @@ export function RecentGroupList() {
       {groupInfo.length > 0 && (
         <>
           <h2 className="mt-6 mb-2">{t('recent')}</h2>
-          <GroupList groups={groupInfo} state={state} setState={setState} />
+          <GroupList
+            groups={groupInfo}
+            groupDetails={data.groups}
+            archivedGroups={archivedGroups}
+            starredGroups={starredGroups}
+            refreshGroupsFromStorage={refreshGroupsFromStorage}
+          />
         </>
       )}
 
@@ -137,8 +174,10 @@ export function RecentGroupList() {
           <div className="opacity-50">
             <GroupList
               groups={archivedGroupInfo}
-              state={state}
-              setState={setState}
+              groupDetails={data.groups}
+              archivedGroups={archivedGroups}
+              starredGroups={starredGroups}
+              refreshGroupsFromStorage={refreshGroupsFromStorage}
             />
           </div>
         </>
@@ -149,12 +188,16 @@ export function RecentGroupList() {
 
 function GroupList({
   groups,
-  state,
-  setState,
+  groupDetails,
+  starredGroups,
+  archivedGroups,
+  refreshGroupsFromStorage,
 }: {
   groups: RecentGroups
-  state: RecentGroupsState
-  setState: (state: SetStateAction<RecentGroupsState>) => void
+  groupDetails?: AppRouterOutput['groups']['list']['groups']
+  starredGroups: string[]
+  archivedGroups: string[]
+  refreshGroupsFromStorage: () => void
 }) {
   return (
     <ul className="grid gap-2 sm:grid-cols-2">
@@ -162,8 +205,12 @@ function GroupList({
         <RecentGroupListCard
           key={group.id}
           group={group}
-          state={state}
-          setState={setState}
+          groupDetail={groupDetails?.find(
+            (groupDetail) => groupDetail.id === group.id,
+          )}
+          isStarred={starredGroups.includes(group.id)}
+          isArchived={archivedGroups.includes(group.id)}
+          refreshGroupsFromStorage={refreshGroupsFromStorage}
         />
       ))}
     </ul>
diff --git a/src/trpc/client.tsx b/src/trpc/client.tsx
index 0c9fc506..7d9065f1 100644
--- a/src/trpc/client.tsx
+++ b/src/trpc/client.tsx
@@ -21,6 +21,8 @@ function getQueryClient() {
   return (clientQueryClientSingleton ??= makeQueryClient())
 }
 
+export const trpcClient = getQueryClient()
+
 function getUrl() {
   const base = (() => {
     if (typeof window !== 'undefined') return ''
diff --git a/src/trpc/routers/groups/get.procedure.ts b/src/trpc/routers/groups/get.procedure.ts
index 02841ffd..331a6fc0 100644
--- a/src/trpc/routers/groups/get.procedure.ts
+++ b/src/trpc/routers/groups/get.procedure.ts
@@ -1,19 +1,10 @@
-import { getGroup, getGroupExpensesParticipants } from '@/lib/api'
+import { getGroup } from '@/lib/api'
 import { baseProcedure } from '@/trpc/init'
-import { TRPCError } from '@trpc/server'
 import { z } from 'zod'
 
 export const getGroupProcedure = baseProcedure
   .input(z.object({ groupId: z.string().min(1) }))
   .query(async ({ input: { groupId } }) => {
     const group = await getGroup(groupId)
-    if (!group) {
-      throw new TRPCError({
-        code: 'NOT_FOUND',
-        message: 'Group not found.',
-      })
-    }
-
-    const participantsWithExpenses = await getGroupExpensesParticipants(groupId)
-    return { group, participantsWithExpenses }
+    return { group }
   })
diff --git a/src/trpc/routers/groups/getDetails.procedure.ts b/src/trpc/routers/groups/getDetails.procedure.ts
new file mode 100644
index 00000000..831b6a85
--- /dev/null
+++ b/src/trpc/routers/groups/getDetails.procedure.ts
@@ -0,0 +1,19 @@
+import { getGroup, getGroupExpensesParticipants } from '@/lib/api'
+import { baseProcedure } from '@/trpc/init'
+import { TRPCError } from '@trpc/server'
+import { z } from 'zod'
+
+export const getGroupDetailsProcedure = baseProcedure
+  .input(z.object({ groupId: z.string().min(1) }))
+  .query(async ({ input: { groupId } }) => {
+    const group = await getGroup(groupId)
+    if (!group) {
+      throw new TRPCError({
+        code: 'NOT_FOUND',
+        message: 'Group not found.',
+      })
+    }
+
+    const participantsWithExpenses = await getGroupExpensesParticipants(groupId)
+    return { group, participantsWithExpenses }
+  })
diff --git a/src/trpc/routers/groups/index.ts b/src/trpc/routers/groups/index.ts
index c4f02d8e..13222883 100644
--- a/src/trpc/routers/groups/index.ts
+++ b/src/trpc/routers/groups/index.ts
@@ -6,6 +6,8 @@ import { groupExpensesRouter } from '@/trpc/routers/groups/expenses'
 import { getGroupProcedure } from '@/trpc/routers/groups/get.procedure'
 import { groupStatsRouter } from '@/trpc/routers/groups/stats'
 import { updateGroupProcedure } from '@/trpc/routers/groups/update.procedure'
+import { getGroupDetailsProcedure } from './getDetails.procedure'
+import { listGroupsProcedure } from './list.procedure'
 
 export const groupsRouter = createTRPCRouter({
   expenses: groupExpensesRouter,
@@ -14,6 +16,8 @@ export const groupsRouter = createTRPCRouter({
   activities: activitiesRouter,
 
   get: getGroupProcedure,
+  getDetails: getGroupDetailsProcedure,
+  list: listGroupsProcedure,
   create: createGroupProcedure,
   update: updateGroupProcedure,
 })
diff --git a/src/trpc/routers/groups/list.procedure.ts b/src/trpc/routers/groups/list.procedure.ts
new file mode 100644
index 00000000..557288aa
--- /dev/null
+++ b/src/trpc/routers/groups/list.procedure.ts
@@ -0,0 +1,14 @@
+import { getGroups } from '@/lib/api'
+import { baseProcedure } from '@/trpc/init'
+import { z } from 'zod'
+
+export const listGroupsProcedure = baseProcedure
+  .input(
+    z.object({
+      groupIds: z.array(z.string().min(1)),
+    }),
+  )
+  .query(async ({ input: { groupIds } }) => {
+    const groups = await getGroups(groupIds)
+    return { groups }
+  })