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 installing

The 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_URLrequired

    PostgreSQL connection string.

    e.g. postgresql://user:pass@localhost:5432/mydb

Install via CLI

npx feature101@latest add follow

Import

import { FollowButton } from '@/features/follow'

Usage

<FollowButton targetUserId="user_xyz" />

Props

PropTypeDescription
targetUserId*
stringID of the user being followed.
showCount
booleanDisplay follower count next to the button.
className
stringCustom CSS classes for styling.
onFollowChange
(isFollowing: boolean) => voidCallback 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.

Files10
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;