ESLint plugin for React and Next.js projects with no-jsx-without-return rule
npm install @laststance/react-next-eslint-pluginESLint plugin for React and Next.js projects that includes one rule for my personal use and a rule to prevent infinite re-renders during Vibe Coding.

``bash`
npm install --save-dev @laststance/react-next-eslint-plugin@latest
`bash`
yarn add --dev @laststance/react-next-eslint-plugin@latest
`bash`
pnpm add --save-dev @laststance/react-next-eslint-plugin@latest
`javascript
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',
{ depth: 1 },
],
'@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/no-forward-ref': 'error',
'@laststance/react-next/no-context-provider': 'error',
'@laststance/react-next/no-missing-key': 'error',
'@laststance/react-next/no-duplicate-key': 'error',
'@laststance/react-next/no-missing-component-display-name': 'error',
'@laststance/react-next/no-nested-component-definitions': 'error',
'@laststance/react-next/no-missing-button-type': 'error',
'@laststance/react-next/prefer-stable-context-value': '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',
},
},
]
`
These rules are provided by the plugin. Enable only those you need. Click on each rule for detailed documentation.
Some rules are imported and adapted from https://github.com/jsx-eslint/eslint-plugin-react.
- laststance/no-jsx-without-return: Disallow JSX elements not returned or assigned
- laststance/all-memo: Enforce wrapping React function components with React.memolaststance/no-use-reducer
- : Disallow useReducer hook in favor of Redux Toolkit to eliminate bugslaststance/no-set-state-prop-drilling
- : Disallow passing useState setters via props; prefer semantic handlers or state managementlaststance/no-deopt-use-callback
- : Flag meaningless useCallback usage with intrinsic elements or inline callslaststance/no-deopt-use-memo
- : Flag meaningless useMemo usage with intrinsic elements or inline handlerslaststance/no-direct-use-effect
- : Disallow calling useEffect directly inside React components; extract to custom hookslaststance/no-forward-ref
- : Prefer passing ref as a prop instead of forwardRef (React 19)laststance/no-context-provider
- : Prefer rendering instead of (React 19)laststance/no-missing-key
- : Disallow list items without keylaststance/no-duplicate-key
- : Disallow duplicate key values among siblingslaststance/no-missing-component-display-name
- : Require displayName for anonymous memo/forwardRef componentslaststance/no-nested-component-definitions
- : Disallow defining components inside other componentslaststance/no-missing-button-type
- : Require explicit type for button elementslaststance/prefer-stable-context-value
- : Prefer stable Context.Provider values (wrap with useMemo/useCallback)laststance/prefer-usecallback-might-work
- : Ensure custom components receive useCallback-stable function propslaststance/prefer-usecallback-for-memoized-component
- : Ensure function props sent to memoized components are wrapped in useCallbacklaststance/prefer-usememo-for-memoized-component
- : Ensure object/array props to memoized components are wrapped in useMemolaststance/prefer-usememo-might-work
- : Ensure custom components receive useMemo-stable object/array props
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:
`bashRun Vitest snapshot tests inside the demo app
pnpm --filter todo-lint-app test
$3
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
$3
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
`javascript
function Component() {
;Hello World // Missing return statement
}function Component() {
if (condition)
Hello // Missing return or block wrapping
}function Component() {
if (condition) {
return
Hello
} else Goodbye // Missing return or block wrapping
}
`✅ Correct
`javascript
function Component() {
return Hello World
}function Component() {
if (condition) {
return
Hello
}
}function Component() {
if (condition) {
return
Hello
} else {
return Goodbye
}
}
`$3
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.This rule ignores the following files:
- Next.js
layout.tsx (Server Components)
- Storybook stories that include .stories. in the filename❌ Incorrect
`javascript
// Function component without memo wrapping
const UserCard = ({ name, email }) => {
return (
{name}
{email}
)
}function ProductItem({ title, price }) {
return (
{title}
${price}
)
}
`✅ Correct
`javascript
import React, { memo } from 'react'// Wrapped with memo
const UserCard = memo(({ name, email }) => {
return (
{name}
{email}
)
})const ProductItem = memo(function ProductItem({ title, price }) {
return (
{title}
${price}
)
})// Assignment style also works
function ProductItemBase({ title, price }) {
return (
{title}: ${price}
)
}
const ProductItem = memo(ProductItemBase)
`$3
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
`javascript
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 (
{state.count}
)
}
`✅ Correct
`javascript
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 (
{count}
)
}
`$3
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
`javascript
import { useState } from 'react'function Parent() {
const [count, setCount] = useState(0)
// Passing setter directly creates tight coupling
return
}
function Child({ setCount, count }) {
return
}
`✅ Correct
`javascript
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
}
function Child({ onIncrement, count }) {
return
}
`Options
-
depth (number, default: 0): allows passing a setter through up to N component levels within the same file. Imported components stop depth propagation.$3
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
`javascript
import { useCallback } from 'react'function Component() {
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return (
{/ Meaningless: intrinsic elements don't benefit from useCallback /}
{/ Meaningless: calling inside inline handler defeats the purpose /}
)
}
`✅ Correct
`javascript
import React, { useCallback, memo } from 'react'const MemoizedButton = memo(function MemoizedButton({ onClick, children }) {
return
})
function Component() {
// Meaningful: stabilizes prop for memoized component
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return (
Click me {/ Or just use inline for intrinsic elements /}
)
}
`$3
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
`javascript
import { useEffect } from 'react'function Dashboard() {
useEffect(() => {
trackPageView('dashboard')
}, [])
return Dashboard
}
`✅ Correct
`javascript
import { useEffect } from 'react'function useDashboardTracking() {
useEffect(() => {
trackPageView('dashboard')
}, [])
}
function Dashboard() {
useDashboardTracking()
return Dashboard
}
`$3
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
`javascript
import React, { createContext, useState } from 'react'const UserContext = createContext(null)
function UserProvider({ children }) {
const [user, setUser] = useState(null)
return (
value={{ user, setUser }} // New object on every render!
>
{children}
)
}
`✅ Correct
`javascript
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 (
{children}
)
}
`$3
In React 19,
forwardRef is no longer required for function components. This rule flags forwardRef usage so you can pass ref as a prop instead.❌ Incorrect
`javascript
const Button = React.forwardRef((props, ref) => {
return
})
`✅ Correct
`javascript
const Button = ({ ref }) => {
return
}
`$3
In React 19,
can be used directly as a provider. This rule warns on .❌ Incorrect
`javascript
const App = () =>
`✅ Correct
`javascript
const App = () =>
`$3
This rule requires
key when rendering lists and discourages fragment shorthand in list items.❌ Incorrect
`javascript
items.map((item) => )items.map((item) => <>{item}>)
`✅ Correct
`javascript
items.map((item) => )items.map((item) => {item} )
`$3
This rule requires sibling elements to have unique
key values.❌ Incorrect
`javascript
return [
,
,
]
`✅ Correct
`javascript
return [
,
,
]
`$3
Anonymous components wrapped with
memo or forwardRef should have an explicit displayName.❌ Incorrect
`javascript
const App = React.memo(() => )
`✅ Correct
`javascript
const App = React.memo(function App() {
return
})App.displayName = 'App'
`$3
Defining components inside other components recreates them on each render. This rule flags nested component definitions.
❌ Incorrect
`javascript
function Parent() {
function Child() {
return
}
return
}
`✅ Correct
`javascript
function Child() {
return
}function Parent() {
return
}
`$3
Buttons should have an explicit
type attribute to avoid implicit submit behavior.❌ Incorrect
`javascript
`✅ Correct
`javascript
``This plugin intentionally does not ship a bundled recommended config. Opt-in the rules that fit your codebase.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © laststance