🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 8 Min Lesezeit
0

Optimistic UI Patterns with Next.js Server Actions and Supabase Realtime

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




Optimistic UI Patterns with Next.js Server Actions and Supabase Realtime



The difference between an app that feels fast and one that feels slow often isn't the actual latency — it's whether the UI responds before the server does. Optimistic updates show the expected result immediately, then reconcile with the server response. Done right, users barely notice network latency.



Next.js 15 ships useOptimistic as a stable React 19 hook, and it integrates cleanly with Server Actions. Combined with Supabase Realtime for multi-user sync, you can build interfaces that feel instant while staying consistent across clients.



Estimated read time: 13 minutes






Prerequisites




  • Next.js 15 (React 19) for stable useOptimistic

  • Supabase project with Realtime enabled

  • Familiarity with Server Actions and useTransition

  • TypeScript recommended









How useOptimistic Works



useOptimistic takes two arguments: the current real state, and an update function that produces the optimistic state. It returns the optimistic state (shown to the user) and a function to trigger an optimistic update.




CODE
const [optimisticState, addOptimistic] = useOptimistic(
realState,
(currentState, optimisticValue) => {
// Return the new optimistic state
return [...currentState, optimisticValue]
}
)






While a Server Action is in flight:





  • optimisticState shows the optimistic value

  • When the action completes, React reverts to realState (which should now reflect the server's response via revalidatePath or revalidateTag)

  • If the action throws, React reverts to the previous realState automatically









Pattern 1: Optimistic List Item Addition



The most common use case — adding an item to a list without waiting for the server.




CODE
// app/todos/TodoList.tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { addTodo } from './actions'

interface Todo {
id: string
text: string
completed: boolean
pending?: boolean // flag for optimistic items
}

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [isPending, startTransition] = useTransition()
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
initialTodos,
(state: Todo[], newTodo: Todo) => [...state, newTodo]
)

async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string
if (!text.trim()) return

const optimisticTodo: Todo = {
id: `temp-${Date.now()}`, // temporary ID
text,
completed: false,
pending: true,
}

startTransition(async () => {
addOptimisticTodo(optimisticTodo)
await addTodo(text)
})
}

return (
<div>
<form action={handleSubmit}>
<input name="text" placeholder="Add todo..." required />
<button type="submit" disabled={isPending}>Add</button>
</form>

<ul>
{optimisticTodos.map((todo) => (
<li
key={todo.id}
style={{ opacity: todo.pending ? 0.6 : 1 }}
>
{todo.text}
{todo.pending && <span> (saving...)</span>}
</li>
))}
</ul>
</div>
)
}









CODE
// app/todos/actions.ts
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function addTodo(text: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')

const { error } = await supabase
.from('todos')
.insert({ text, user_id: user.id, completed: false })

if (error) throw error

revalidatePath('/todos')
}






When revalidatePath runs, Next.js re-fetches the Server Component data. The real todo (with a real ID from the database) replaces the optimistic one.









Pattern 2: Optimistic Toggle (Like / Complete)



Toggles are the simplest optimistic pattern — the new state is the inverse of the current state.




CODE
// app/todos/TodoItem.tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { toggleTodo } from './actions'

interface Todo {
id: string
text: string
completed: boolean
}

export function TodoItem({ todo }: { todo: Todo }) {
const [, startTransition] = useTransition()
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
todo.completed,
(_, newValue: boolean) => newValue
)

function handleToggle() {
startTransition(async () => {
setOptimisticCompleted(!optimisticCompleted)
await toggleTodo(todo.id, !todo.completed)
})
}

return (
<li
onClick={handleToggle}
style={{
textDecoration: optimisticCompleted ? 'line-through' : 'none',
cursor: 'pointer',
}}
>
{todo.text}
</li>
)
}






The toggle feels instant. If the server action fails, optimisticCompleted reverts to todo.completed automatically.









Pattern 3: Optimistic Delete with Error Recovery



Deletes need careful handling — if the delete fails, the item must reappear.




CODE
// app/todos/TodoList.tsx
'use client'

import { useOptimistic, useTransition, useState } from 'react'
import { deleteTodo } from './actions'

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)

const [optimisticTodos, removeOptimisticTodo] = useOptimistic(
initialTodos,
(state: Todo[], idToRemove: string) =>
state.filter((t) => t.id !== idToRemove)
)

function handleDelete(id: string) {
setError(null)
startTransition(async () => {
removeOptimisticTodo(id)
try {
await deleteTodo(id)
} catch (err: any) {
setError(`Failed to delete: ${err.message}`)
// useOptimistic auto-reverts — the item reappears
}
})
}

return (
<div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}






The try/catch inside startTransition catches the Server Action error and shows it to the user. useOptimistic reverts the state automatically because the transition completed with the original state.









Pattern 4: Combining Optimistic Updates with Supabase Realtime



In multi-user apps, you need both optimistic updates (for the current user) and Realtime updates (for other users). The challenge is avoiding duplicate updates.




CODE
// app/todos/RealtimeTodoList.tsx
'use client'

import { createClient } from '@/lib/supabase/client'
import { useOptimistic, useTransition, useEffect, useState } from 'react'
import { addTodo } from './actions'

export function RealtimeTodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [todos, setTodos] = useState(initialTodos)
const [pendingIds] = useState(() => new Set<string>())
const [, startTransition] = useTransition()

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state: Todo[], newTodo: Todo) => [...state, newTodo]
)

// Subscribe to Realtime changes from OTHER users
useEffect(() => {
const supabase = createClient()

const channel = supabase
.channel('todos')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'todos' },
(payload) => {
const newTodo = payload.new as Todo

// Skip if this is our own optimistic update
if (pendingIds.has(newTodo.id)) {
pendingIds.delete(newTodo.id)
return
}

// Add todo from another user
setTodos((prev) => {
if (prev.find((t) => t.id === newTodo.id)) return prev
return [...prev, newTodo]
})
}
)
.subscribe()

return () => { supabase.removeChannel(channel) }
}, [pendingIds])

async function handleAdd(formData: FormData) {
const text = formData.get('text') as string
const tempId = `temp-${Date.now()}`

const optimisticTodo: Todo = {
id: tempId,
text,
completed: false,
pending: true,
}

startTransition(async () => {
addOptimisticTodo(optimisticTodo)
const realId = await addTodo(text) // returns the real DB id
if (realId) pendingIds.add(realId) // mark to skip in Realtime handler
})
}

return (
<form action={handleAdd}>
<input name="text" />
<button type="submit">Add</button>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
{todo.text}
</li>
))}
</ul>
</form>
)
}






The pendingIds set tracks IDs of items we just inserted. When Realtime broadcasts the insert, we check if it's one of ours and skip it to avoid duplication.



[INTERNAL LINK: nextjs-supabase-realtime-collaboration]









Pattern 5: Optimistic Updates for Forms with Validation



For forms with server-side validation, you want to show the optimistic state but handle validation errors gracefully:




CODE
// app/profile/ProfileForm.tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { updateProfile } from './actions'

interface Profile {
displayName: string
bio: string
}

export function ProfileForm({ profile }: { profile: Profile }) {
const [, startTransition] = useTransition()
const [optimisticProfile, setOptimisticProfile] = useOptimistic(
profile,
(_, newProfile: Profile) => newProfile
)

async function handleSubmit(formData: FormData) {
const newProfile = {
displayName: formData.get('displayName') as string,
bio: formData.get('bio') as string,
}

startTransition(async () => {
setOptimisticProfile(newProfile)
const result = await updateProfile(newProfile)

if (result?.error) {
// Server action returned a validation error
// useOptimistic reverts automatically
// Show error to user via some state mechanism
}
})
}

return (
<form action={handleSubmit}>
<input name="displayName" defaultValue={optimisticProfile.displayName} />
<textarea name="bio" defaultValue={optimisticProfile.bio} />
<button type="submit">Save</button>
</form>
)
}












Common Pitfalls



Not wrapping addOptimistic in startTransition. useOptimistic only works inside a React transition. Calling addOptimistic outside startTransition will throw in development and silently fail in production.



Using the temporary ID in subsequent operations. The optimistic item has a fake ID (temp-${Date.now()}). If the user tries to delete or update it before the real ID arrives, the operation will fail. Disable action buttons on items with pending: true.



Forgetting revalidatePath in the Server Action. Without revalidation, the Server Component data doesn't update after the action completes. The optimistic state reverts to the stale real state, making it look like the action failed.



Race conditions with rapid successive actions. If a user clicks "add" multiple times quickly, multiple optimistic updates queue up. Each one reverts independently when its action completes. This is usually fine, but test it explicitly.









Summary and Next Steps



useOptimistic + Server Actions is the cleanest optimistic update pattern in the Next.js ecosystem. The key insight: optimistic state is temporary and automatically reverts — your job is to make the real state catch up via revalidatePath, and to handle errors gracefully so users know when something went wrong.



For multi-user apps, combine optimistic updates with Supabase Realtime and deduplicate events from your own mutations using a pending ID set.



Related reading:




  • [INTERNAL LINK: nextjs-server-actions-supabase-complete-guide]

  • [INTERNAL LINK: nextjs-supabase-realtime-collaboration]

  • [INTERNAL LINK: nextjs-supabase-data-fetching-patterns]






Originally published at https://www.iloveblogs.blog

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Optimistic UI Patterns with Next.js Server Actions and Supabase Realtime

Thematisch verwandte Begriffe: Optimistic, Patterns, with, Nextjs · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...