Follow
Add follow/unfollow functionality to user profiles.
@prisma/client@prisma/adapter-pgpgdotenvzustandprisma@types/pg
shadcn/uibuttonskeletonauto-installed by CLI
Preview
23 followers
Add these to your
.env before installingThe CLI checks for these at install time — missing values mean the database step gets skipped, and the feature won't work until it's added.
DATABASE_URLrequiredPostgreSQL connection string.
e.g. postgresql://user:pass@localhost:5432/mydb
Install via CLI
npx feature101@latest add followImport
import { FollowButton } from '@/features/follow'Usage
<FollowButton targetUserId="user_xyz" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
targetUserId* | string | — | ID of the user being followed. |
showCount | boolean | true | Display follower count next to the button. |
className | string | — | Custom CSS classes for styling. |
onFollowChange | (isFollowing: boolean) => void | — | Callback fired with real server result on confirm only. Never fires on optimistic update or rollback. |
Data Flow
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
Files10
Files
components/FollowButton.tsx
"use client";
import { memo } from "react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useFollow } from "../follow.hooks";
import type { FollowButtonProps } from "../follow.types";
const FollowButton = memo(
({
targetUserId,
showCount = true,
className = "",
onFollowChange,
}: FollowButtonProps) => {
const { isFollowing, followerCount, isFetching, error, toggleFollow } =
useFollow({ targetUserId, onFollowChange });
if (isFetching) {
return (
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-20 rounded-md" />
{showCount && <Skeleton className="h-4 w-16 rounded" />}
</div>
);
}
return (
<div className={`inline-flex items-center gap-2 ${className}`}>
<Button
onClick={toggleFollow}
variant={isFollowing ? "outline" : "default"}
aria-label={isFollowing ? "Unfollow" : "Follow"}
>
{isFollowing ? "Following" : "Follow"}
</Button>
{showCount && (
<span className="text-sm text-muted-foreground tabular-nums">
{followerCount} {followerCount === 1 ? "follower" : "followers"}
</span>
)}
{error && <span className="text-xs text-destructive">{error}</span>}
</div>
);
},
);
FollowButton.displayName = "FollowButton";
export default FollowButton;