Skip to content
IC Reactor

All-in-One Demo

A simplified “Twitter-like” dApp that demonstrates the full power of IC Reactor, including optimistic UI updates, backend integration, and authentication.

  • Optimistic Updates: UI updates instantly when you “Like” a post, rolling back on error.
  • Motoko Backend: Includes a simple Motoko canister for posts and likes.
  • Generated Reactor: The Vite plugin generates the backend’s DisplayReactor and hooks into src/declarations/backend/ from its Candid.
  • Authentication: Connect with Internet Identity.
  • Form Handling: Creating new posts with loading states.
  • Automatic Refetching: Lists update automatically after mutations.
src/lib/factories.ts
import { createMutation, createQuery } from "@ic-reactor/react"
import { backendReactor } from "../declarations/backend"
export const getLikes = createQuery(backendReactor, {
functionName: "get_likes",
refetchInterval: 3000,
})
export const likeHeart = createMutation(backendReactor, {
functionName: "like",
})

onCanisterError fires only for canister Result { Err } variants and receives two arguments — the CanisterError and the mutation variables. onError fires for every error and is the one that receives the onMutate context.

The demo’s useHeart(addLog) hook, in src/lib/useHeart.ts, returns { likesCount, isLiked, isLikesLoading, handleHeartClick, savedUpdates }. This excerpt and the next each show one part of it, as a hook of its own:

// An excerpt of useHeart (src/lib/useHeart.ts): its mutation callbacks
import type { FrontendLog } from "./types"
import { getLikes, likeHeart } from "./factories"
export function useLikeMutation(
addLog: (type: FrontendLog["type"], message: string) => void
) {
const { mutateAsync: likeMutate } = likeHeart.useMutation({
// Canister returned { Ok }
onSuccess: () => {
addLog("success", "Like confirmed by canister")
},
// Canister returned { Err } — business logic errors only
onCanisterError: (error, variables) => {
addLog("error", `Canister rejected like: ${error.message}`)
},
// Every error, including network and agent failures
onError: (error, variables, context) => {
console.error("Like mutation error:", error)
},
onSettled: () => getLikes.invalidate(),
})
return { likeMutate }
}

The demo’s useHeart uses React’s useOptimistic rather than writing into the query cache, so the optimistic state unwinds by itself when the transition settles. Here is that part on its own (the demo keeps it inside useHeart, next to the two mutations and the debounce):

// An excerpt of useHeart (src/lib/useHeart.ts): its optimistic state
import { useOptimistic } from "react"
import { getLikes } from "./factories"
export function useOptimisticLikes() {
const { data: likes = [] } = getLikes.useQuery()
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state, newLike: { type: "add" | "remove"; principal: string }) =>
newLike.type === "add"
? [...state, newLike.principal]
: state.filter((p) => p !== newLike.principal)
)
return { likesCount: optimisticLikes.length, addOptimisticLike }
}