Follow

Engagement

Add follow/unfollow functionality to user profiles.

@prisma/client@prisma/adapter-pgpgdotenvzustandprisma@types/pg
shadcn/uibuttonskeleton— auto-installed by CLI
9 files generated

Preview

23 followers
Before running the command, ensure these are in your.env

Without these in your.env your database won't be set up — the feature will break at runtime.

  • 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 currentUserId="user_123" targetUserId="user_xyz" />

Props

PropTypeDescription
currentUserId*
stringID of the user clicking follow.
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 when follow state changes.
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
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(
  ({
    currentUserId,
    targetUserId,
    showCount = true,
    className = "",
    onFollowChange,
  }: FollowButtonProps) => {
    const {
      isFollowing,
      followerCount,
      isLoading,
      isFetching,
      error,
      toggleFollow,
    } = useFollow({ currentUserId, targetUserId });

    const handleClick = async () => {
      await toggleFollow();
      onFollowChange?.(!isFollowing);
    };

    if (currentUserId === targetUserId) return null;

    // Initial fetch in flight — show skeleton matching button + count dimensions
    if (isFetching) {
      return (
        <div className="inline-flex items-center gap-2">
          <Skeleton className="h-9 w-24 rounded-md" />
          {showCount && <Skeleton className="h-4 w-20 rounded-md" />}
        </div>
      );
    }

    return (
      <div className={`inline-flex items-center gap-2 ${className}`}>
        <Button
          onClick={handleClick}
          disabled={isLoading}
          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;