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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@laststance/react-next-eslint-plugin

v1.1.0

Published

ESLint plugin for React and Next.js projects with no-jsx-without-return rule

Downloads

933

Readme

@laststance/react-next-eslint-plugin

ESLint plugin for React and Next.js projects with rules to improve code quality and catch common mistakes.

Installation

npm install --save-dev @laststance/react-next-eslint-plugin@latest
yarn add --dev @laststance/react-next-eslint-plugin@latest
pnpm add --save-dev @laststance/react-next-eslint-plugin@latest

Usage

Flat Config (ESLint 9.0+)

import lastStanceReactNextPlugin from '@laststance/react-next-eslint-plugin'

export default [
  {
    plugins: {
      '@laststance/react-next': lastStanceReactNextPlugin,
    },
    rules: {
      '@laststance/react-next/no-jsx-without-return': 'error',
      '@laststance/react-next/all-memo': 'error',
      '@laststance/react-next/no-use-reducer': 'error',
      '@laststance/react-next/no-set-state-prop-drilling': 'error',
      '@laststance/react-next/no-deopt-use-callback': 'error',
      '@laststance/react-next/no-deopt-use-memo': 'error',
      '@laststance/react-next/no-direct-use-effect': 'error',
      '@laststance/react-next/prefer-stable-context-value': 'error',
      '@laststance/react-next/no-unstable-classname-prop': 'error',
      '@laststance/react-next/prefer-usecallback-might-work': 'error',
      '@laststance/react-next/prefer-usecallback-for-memoized-component': 'error',
      '@laststance/react-next/prefer-usememo-for-memoized-component': 'error',
      '@laststance/react-next/prefer-usememo-might-work': 'error',
    },
  },
]

Rules

These rules are provided by the plugin. Enable only those you need. Click on each rule for detailed documentation.

Monorepo Workspace & Demo App

The repository now uses a pnpm workspace (pnpm-workspace.yaml). In addition to the plugin package located at the root, there is a Next.js TODO playground under apps/todo-lint-app that intentionally mixes code which should pass/fail the custom rules.

  • apps/todo-lint-app: Generated with create-next-app, wired to consume the local plugin, and equipped with Vitest snapshot tests that execute ESLint and capture its output.

See docs/demo-playground.md for detailed guidance on when and how to refresh the playground snapshot.

Useful commands:

# Run Vitest snapshot tests inside the demo app
pnpm --filter todo-lint-app test

# Update the stored ESLint snapshot after rule/message changes
pnpm --filter todo-lint-app test -- --update

# Lint only the demo app using the workspace plugin build
pnpm --filter todo-lint-app lint

TypeScript Support

The published package ships index.d.ts typings so flat-config files can import the plugin with autocomplete. Run pnpm typecheck to ensure the declaration files stay in sync when adding new rules.

Rule Details

no-jsx-without-return

This rule prevents JSX elements that are not properly returned or assigned, which typically indicates a missing return statement. It specifically catches standalone JSX expressions and JSX in if/else statements without proper return handling.

❌ Incorrect

function Component() {
  ;<div>Hello World</div> // Missing return statement
}

function Component() {
  if (condition) <div>Hello</div> // Missing return or block wrapping
}

function Component() {
  if (condition) {
    return <div>Hello</div>
  } else <div>Goodbye</div> // Missing return or block wrapping
}

✅ Correct

function Component() {
  return <div>Hello World</div>
}

function Component() {
  if (condition) {
    return <div>Hello</div>
  }
}

function Component() {
  if (condition) {
    return <div>Hello</div>
  } else {
    return <div>Goodbye</div>
  }
}

all-memo

This rule enforces that all React function components (PascalCase functions returning JSX) are wrapped with React.memo to prevent unnecessary re-renders and improve performance.

❌ Incorrect

// Function component without memo wrapping
const UserCard = ({ name, email }) => {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

function ProductItem({ title, price }) {
  return (
    <div>
      <h4>{title}</h4>
      <span>${price}</span>
    </div>
  )
}

✅ Correct

import React, { memo } from 'react'

// Wrapped with memo
const UserCard = memo(({ name, email }) => {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
})

const ProductItem = memo(function ProductItem({ title, price }) {
  return (
    <div>
      <h4>{title}</h4>
      <span>${price}</span>
    </div>
  )
})

// Assignment style also works
function ProductItemBase({ title, price }) {
  return (
    <div>
      {title}: ${price}
    </div>
  )
}
const ProductItem = memo(ProductItemBase)

no-use-reducer

This rule discourages the use of useReducer hook in favor of Redux Toolkit to eliminate the possibility of introducing bugs through complex state management logic and provide better developer experience.

❌ Incorrect

import { useReducer } from 'react'

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  )
}

✅ Correct

import { useSelector, useDispatch } from 'react-redux'
import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => {
      state.count += 1
    },
    decrement: (state) => {
      state.count -= 1
    },
  },
})

function Counter() {
  const count = useSelector((state) => state.counter.count)
  const dispatch = useDispatch()

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(counterSlice.actions.increment())}>
        +
      </button>
      <button onClick={() => dispatch(counterSlice.actions.decrement())}>
        -
      </button>
    </div>
  )
}

no-set-state-prop-drilling

This rule prevents passing useState setter functions directly through props, which creates tight coupling and can cause unnecessary re-renders due to unstable function identity. Instead, it promotes semantic handlers or proper state management.

❌ Incorrect

import { useState } from 'react'

function Parent() {
  const [count, setCount] = useState(0)

  // Passing setter directly creates tight coupling
  return <Child setCount={setCount} count={count} />
}

function Child({ setCount, count }) {
  return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
}

✅ Correct

import { useState, useCallback } from 'react'

function Parent() {
  const [count, setCount] = useState(0)

  // Semantic handler with clear intent
  const handleIncrement = useCallback(() => {
    setCount((c) => c + 1)
  }, [])

  return <Child onIncrement={handleIncrement} count={count} />
}

function Child({ onIncrement, count }) {
  return <button onClick={onIncrement}>Count: {count}</button>
}

no-deopt-use-callback

This rule detects meaningless uses of useCallback where the function is passed to intrinsic elements (like div, button) or called inside inline handlers. useCallback should primarily stabilize function props for memoized components to preserve referential equality.

❌ Incorrect

import { useCallback } from 'react'

function Component() {
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return (
    <div>
      {/* Meaningless: intrinsic elements don't benefit from useCallback */}
      <button onClick={handleClick}>Click me</button>

      {/* Meaningless: calling inside inline handler defeats the purpose */}
      <button onClick={() => handleClick()}>Click me too</button>
    </div>
  )
}

✅ Correct

import React, { useCallback, memo } from 'react'

const MemoizedButton = memo(function MemoizedButton({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>
})

function Component() {
  // Meaningful: stabilizes prop for memoized component
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return (
    <div>
      <MemoizedButton onClick={handleClick}>Click me</MemoizedButton>

      {/* Or just use inline for intrinsic elements */}
      <button onClick={() => console.log('clicked')}>Simple click</button>
    </div>
  )
}

no-direct-use-effect

This rule discourages calling useEffect directly inside React components so that side effects live in focused custom hooks. Keeping components declarative makes them easier to test and reuse.

❌ Incorrect

import { useEffect } from 'react'

function Dashboard() {
  useEffect(() => {
    trackPageView('dashboard')
  }, [])

  return <main>Dashboard</main>
}

✅ Correct

import { useEffect } from 'react'

function useDashboardTracking() {
  useEffect(() => {
    trackPageView('dashboard')
  }, [])
}

function Dashboard() {
  useDashboardTracking()
  return <main>Dashboard</main>
}

prefer-stable-context-value

This rule prevents passing new object/array/function literals to Context.Provider values on each render, which causes unnecessary re-renders of all context consumers. Values should be wrapped with useMemo or useCallback.

❌ Incorrect

import React, { createContext, useState } from 'react'

const UserContext = createContext(null)

function UserProvider({ children }) {
  const [user, setUser] = useState(null)

  return (
    <UserContext.Provider
      value={{ user, setUser }} // New object on every render!
    >
      {children}
    </UserContext.Provider>
  )
}

✅ Correct

import React, { createContext, useState, useMemo } from 'react'

const UserContext = createContext(null)

function UserProvider({ children }) {
  const [user, setUser] = useState(null)

  // Stable reference prevents unnecessary consumer re-renders
  const contextValue = useMemo(
    () => ({
      user,
      setUser,
    }),
    [user],
  )

  return (
    <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>
  )
}

no-unstable-classname-prop

This rule prevents unstable className expressions that change identity on every render, which can cause performance issues in memoized components. It flags inline objects, arrays, function calls, and string concatenations.

❌ Incorrect

function Component({ isActive, theme }) {
  return (
    <div>
      {/* Object literal creates new reference each render */}
      <button className={{ active: isActive, theme }}>Button 1</button>

      {/* Array literal creates new reference each render */}
      <button className={['btn', isActive && 'active']}>Button 2</button>

      {/* Function call executes each render */}
      <button className={classNames('btn', { active: isActive })}>
        Button 3
      </button>

      {/* String concatenation creates new string each render */}
      <button className={'btn ' + theme}>Button 4</button>
    </div>
  )
}

✅ Correct

import { useMemo } from 'react'
import classNames from 'classnames'

function Component({ isActive, theme }) {
  // Memoize complex className logic
  const buttonClassName = useMemo(
    () => classNames('btn', { active: isActive }, theme),
    [isActive, theme],
  )

  return (
    <div>
      {/* Static strings are fine */}
      <button className="btn primary">Static Button</button>

      {/* Template literals with stable references */}
      <button className={`btn ${theme}`}>Template Button</button>

      {/* Memoized complex logic */}
      <button className={buttonClassName}>Complex Button</button>
    </div>
  )
}

Configuration

This plugin intentionally does not ship a bundled recommended config. Opt-in the rules that fit your codebase.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © laststance