npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ryanmab/biome-plugin-avoidable-anti-patterns

v0.0.4

Published

A collection of opinionated Biome rules for identifying and avoiding common code anti-patterns.

Readme

Biome: Avoidable Anti-Patterns

A collection of rules for Biome for identifying and avoiding anti-patterns in code.

These rules are largely inspired by online resources and existing plugins for other linters. Though many of the current rules focus on React Effects, other common anti-patterns are welcome where they can be reasonably detected without introducing too many false positives.

In some ways, this repository is an experiment in capturing anti-patterns that aren't currently (and may never be) native to Biome, with the goal being to provide opinionated machine-readable feedback that helps both humans and AI coding tools avoid common anti-patterns automatically.

Setup

Install package:

# NPM
npm install --save-dev @ryanmab/biome-plugin-avoidable-anti-patterns

# Bun
bun add --dev @ryanmab/biome-plugin-avoidable-anti-patterns

# Yarn
yarn add --dev @ryanmab/biome-plugin-avoidable-anti-patterns

Add the following rules to Biome's configuration file (biome.json):

{
    "plugins": [
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectInitialiseState.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectDeriveState.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectChain.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectFetchWithoutCleanup.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectPassStateToParent.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectEventHandler.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectResetState.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectSetStateOnPropChange.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectSyncExternalStore.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectEmpty.grit",
        "./node_modules/@ryanmab/biome-plugin-avoidable-anti-patterns/src/react-effect/noEffectManageParent.grit"
    ]
}

Rules

[!WARNING] The ways in which anti-patterns can develop are practically endless, and this plugin is by no means exhaustive.

If you spot any false positives or negatives, have suggestions for additional rules, or have other feedback, please open an issue or pull request!

React

noEffectInitialiseState

Disallow using an Effect to initialise state.

import { useEffect, useState } from "react";

function Component() {
	const [state, setState] = useState<string>();

	useEffect(() => {
		// 🔴 Avoid: initialising state in an Effect. Instead, initialise with "Hello World". For
        // SSR hydration, prefer "useSyncExternalStore".
		setState("Hello World");
	}, []);
}

Source: Avoiding Hydration Mismatches with useSyncExternalStore

noEffectDeriveState

Disallow using an Effect to derive new state based on existing state or props.

import { useEffect, useState } from "react";

function Form() {
	const [firstName, setFirstName] = useState("Taylor");
	const [lastName, setLastName] = useState("Swift");

	// 🔴 Avoid: redundant state and unnecessary Effect
	const [fullName, setFullName] = useState("");

	useEffect(() => {
		setFullName(firstName + " " + lastName);
	}, [firstName, lastName]);
}

Source: You Might Not Need an Effect

noEffectChain

Disallow using chains of Effects to trigger each other.

import { useEffect, useState } from "react";

function Game() {
	const [card, setCard] = useState(null);
	const [goldCardCount, setGoldCardCount] = useState(0);
	const [round, setRound] = useState(1);
	const [isGameOver, setIsGameOver] = useState(false);

	// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
	useEffect(() => {
		if (card !== null) {
			setGoldCardCount((c) => c + 1);
		}
	}, [card]);

	useEffect(() => {
		if (goldCardCount > 3) {
			setRound((r) => r + 1);
			setGoldCardCount(0);
		}
	}, [goldCardCount]);

	useEffect(() => {
		if (round > 5) {
			setIsGameOver(true);
		}
	}, [round]);

	useEffect(() => {
		alert("Good game!");
	}, [isGameOver]);
}

Source: You Might Not Need an Effect

noEffectFetchWithoutCleanup

Disallow using an Effect to trigger a network request without an accompanying cleanup function on unmount.

import { useEffect, useState } from "react";

function SearchResults(query: string) {
	const [results, setResults] = useState([]);
	const [page, setPage] = useState(1);

	useEffect(() => {
		// 🔴 Avoid: Fetching without cleanup logic
		fetch(query, {}).then(async (json) => {
			setResults(await json.json());
		});
	}, [query, page]);
}

Source: You Might Not Need an Effect

noEffectPassStateToParent

Disallow using an Effect to pass internal state to the parent component.

import { useEffect, useState } from "react";

const isCloserToRightEdge = (e: unknown) => true;

function Toggle(onChange: (isOn: boolean) => void) {
	const [isOn, setIsOn] = useState(false);

	// 🔴 Avoid: The onChange handler runs too late
	useEffect(() => {
		onChange(isOn);
	}, [isOn, onChange]);

	function handleClick() {
		setIsOn(!isOn);
	}

	function handleDragEnd(e: unknown) {
		if (isCloserToRightEdge(e)) {
			setIsOn(true);
		} else {
			setIsOn(false);
		}
	}
}

Source: You Might Not Need an Effect

noEffectEventHandler

Disallow using an Effect for event-specific logic.

import { useEffect } from "react";

const showNotification = (message: string) => {};
const navigateTo = (path: string) => {};

type Product = {
	name: string;
	isInCart: boolean;
};

function ProductPage({
	product,
	addToCart,
}: {
	product: Product;
	addToCart: (product: Product) => void;
}) {
	// 🔴 Avoid: Event-specific logic inside an Effect
	useEffect(() => {
		if (product.isInCart) {
			showNotification(`Added ${product.name} to the shopping cart!`);
		}
	}, [product]);

	function handleBuyClick() {
		addToCart(product);
	}

	function handleCheckoutClick() {
		addToCart(product);
		navigateTo("/checkout");
	}
}

Source: You Might Not Need an Effect

noEffectResetState

Disallow using an Effect solely to reset state to its initial value.

import { useEffect, useState } from "react";

function ProfilePage(userId: number) {
	const [comment, setComment] = useState("");

	// 🔴 Avoid: Resetting state on prop change in an Effect
	useEffect(() => {
		setComment("");
	}, [userId]);
}

Source: You Might Not Need an Effect

noEffectSetStateOnPropChange

Disallow using an Effect to set state based solely on props.

import { useEffect, useState } from "react";

function List(items: unknown) {
	const [isReverse, setIsReverse] = useState(false);
	const [selection, setSelection] = useState(null);

	// 🔴 Avoid: Adjusting state on prop change in an Effect
	useEffect(() => {
		setSelection(null);
	}, [items]);
}

Source: You Might Not Need an Effect

noEffectSyncExternalStore

Disallow using an Effect to synchronise state with an external store.

import { useEffect, useState } from "react";

function useOnlineStatus() {
	// Not ideal: Manual store subscription in an Effect
	const [isOnline, setIsOnline] = useState(true);
	useEffect(() => {
		function updateState() {
			setIsOnline(navigator.onLine);
		}

		updateState();

		window.addEventListener("online", updateState);
		window.addEventListener("offline", updateState);
		return () => {
			window.removeEventListener("online", updateState);
			window.removeEventListener("offline", updateState);
		};
	}, []);

	return isOnline;
}

Source: You Might Not Need an Effect

noEffectEmpty

Disallow using an Effect which is empty.

import { useEffect } from "react";

function Game() {
	const count = 0;

	// 🔴 Avoid: Effects with no body
	useEffect(() => {
        // Empty!
    }, []);

	useEffect(() => {
		// Empty!
	}, [count]);
}

noEffectManageParent

Disallow using an Effect which only depends on props.

import { useEffect } from "react";

function Modal(
	arg: boolean,
	{ isOpen, close }: { isOpen: boolean; close: () => void },
) {
	useEffect(() => {
		// do something
	}, [arg, isOpen, close]);

	return <></>;
}

Limitations

Derived State and Properties

Due to limitations in GritQL, the query language used by Biome plugins, rules are not able to recursively pattern-match, and therefore are unable to follow variable references or chains of callbacks. This means rules fall short of identifying anti-patterns in highly dynamic or obfuscated code.

Most rules attempt to identify anti-patterns one level of derivation deep - though this is not always guaranteed:

import { useEffect, useState } from "react";

function Form() {
	const [firstName, setFirstName] = useState("Taylor");
	const [lastName, setLastName] = useState("Swift");

	const [fullName, setFullName] = useState("");

	useEffect(() => {
        // Caught by `noEffectDeriveState`
		setFullName(firstName + " " + lastName);
	}, [firstName, lastName]);

	useEffect(() => {
        // Caught by `noEffectDeriveState`
        const newFullName = firstName + " " + lastName;
		setFullName(newFullName);
	}, [firstName, lastName]);

	useEffect(() => {
        // NOT caught by `noEffectDeriveState`
        const newFullName = firstName + " " + lastName;
        const extraNewFullName = newFullName;
		setFullName(extraNewFullName);
	}, [firstName, lastName]);
}

Acknowledgements