shadimage
lens-blocks
componentfollow-buttonsource: ok · hit

Lens Follow Button

A button component to follow or unfollow a Lens Account, displaying the current follow state.

npx shadcn add "https://lensblocks.com/r/follow-button.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@lens-protocol/react@0.0.0-canary-20250820102520@lens-protocol/client@0.0.0-canary-20250820102520viem@2.33.0

Registry dependencies

buttonspinner

Files2 · source cached 10/8/2026, 4:09:06 PM

registry/new-york/components/account/lens-follow-button.tsx
"use client";

import { useEffect, useState } from "react";
import { Button } from "@/registry/new-york/ui/button";
import { Account, SessionClient, TxHash, UnauthenticatedError } from "@lens-protocol/react";
import { follow, unfollow } from "@lens-protocol/client/actions";
import { handleOperationWith } from "@lens-protocol/client/viem";
import { WalletClient } from "viem";
import { Spinner } from "@/registry/new-york/ui/spinner";
import { isResult, Result } from "@/registry/new-york/lib/result";

type Props = {
  account: Account | Result<Account>;
  session: Result<SessionClient>;
  wallet?: { data: WalletClient | undefined | null; isLoading?: boolean; error?: unknown };
  onFollowSuccess?: (account: Account, txHash: TxHash) => void;
  onUnfollowSuccess?: (account: Account, txHash: TxHash) => void;
  onFollowError?: (account: Account, error: Error) => void;
};

export const LensFollowButton = ({ ...props }: Props) => {
  const { account: accountRes, session, wallet, onFollowSuccess, onUnfollowSuccess, onFollowError } = props;
  const [isSubmitting, setIsSubmitting] = useState(false);

  const account = isResult(accountRes) ? accountRes?.data : accountRes;
  const sessionClient = session?.data;
  const walletClient = wallet?.data;

  const accountLoading = accountRes && "loading" in accountRes ? accountRes.loading : false;

  const [optimisticIsFollowed, setOptimisticIsFollowed] = useState<boolean>(
    account?.operations?.isFollowedByMe ?? false,
  );

  useEffect(() => {
    setOptimisticIsFollowed(account?.operations?.isFollowedByMe ?? false);
  }, [account?.operations]);

  const handleUnfollow = async () => {
    if (!account || !sessionClient) return;

    if (!walletClient) {
      onFollowError?.(account, new Error("Wallet client is not available"));
      return;
    }

    const res = await unfollow(sessionClient, {
      account: account.address,
    })
      .andThen(handleOperationWith(walletClient))
      .andThen(sessionClient.waitForTransaction);

    if (res.isErr()) {
      onFollowError?.(account, res.error);
      return;
    }

    setOptimisticIsFollowed(false);
    onUnfollowSuccess?.(account, res.value);
  };

  const handleFollow = async () => {
    if (!account || !sessionClient) return;

    if (!walletClient) {
      onFollowError?.(account, new Error("Wallet client is not available"));
      return;
    }

    const res = await follow(sessionClient, {
      account: account.address,
    })
      .andThen(handleOperationWith(walletClient))
      .andThen(sessionClient.waitForTransaction);

    if (res.isErr()) {
      onFollowError?.(account, res.error);
      return;
    }

    setOptimisticIsFollowed(true);
    onFollowSuccess?.(account, res.value);
  };

  const onFollowButtonClick = async () => {
    if (!account) return;

    if (!session) {
      onFollowError?.(account, new UnauthenticatedError());
      return;
    }

    setIsSubmitting(true);

    try {
      if (optimisticIsFollowed) {
        await handleUnfollow();
      } else {
        await handleFollow();
      }
    } catch (e) {
      if (e instanceof Error) {
        onFollowError?.(account, e);
      }
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Button
      variant={optimisticIsFollowed ? "outline" : "default"}
      className="group hover:border-destructive"
      onClick={onFollowButtonClick}
      disabled={isSubmitting || accountLoading || session?.loading || wallet?.isLoading}
    >
      {isSubmitting ? (
        <>
          <Spinner /> {optimisticIsFollowed ? "Unfollowing..." : "Following..."}
        </>
      ) : (
        <>
          <span className="hidden group-hover:inline">{optimisticIsFollowed ? "Unfollow" : "Follow"}</span>
          <span className="inline group-hover:hidden">{optimisticIsFollowed ? "Following" : "Follow"}</span>
        </>
      )}
    </Button>
  );
};
registry/new-york/lib/result.ts
export type Result<T, E = never> = {
  data: T | null | undefined;
  error: E | null | undefined;
  loading: boolean;
};

export const isResult = <T, E = never>(result: any): result is Result<T, E> => {
  return "data" in result && "error" in result && "loading" in result;
};

export const isLoading = (result: any): boolean => {
  if (isResult(result)) {
    return result.loading;
  }
  return false;
};

More from lens-blocks

Browse all components in this library →