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

eslint-plugin-react-memo-primitives

v1.3.1

Published

Enforce React.memo on components with primitive props, and flag unnecessary React.memo on components with no props

Readme

eslint-plugin-react-memo-primitives

An ESLint plugin that enforces the use of React.memo for functional React components that receive primitive props, flags React.memo used on components that don't receive any props at all or that receive a non-primitive prop, and requires a displayName on memoized components.

Rules

react-memo-primitives/require-memo-primitives

Flags a functional component (arrow function, function expression, or function declaration) that returns JSX and destructures a single object parameter of props, based on whether all of those props are primitive (string, number, boolean, bigint, null, undefined, literal types, or unions/intersections of those):

  • If every prop is primitive and the component isn't wrapped in memo(...) / React.memo(...), memo is required.
  • If any prop is non-primitive (object, function, ref, or other unresolvable type) and the component is wrapped in memo(...) / React.memo(...), memo is flagged as unnecessary — a non-primitive prop can still change identity on every render, so memo buys nothing.

When the parameter has a TS type annotation, each prop's actual declared type is checked — a function, ref, or object-shaped prop (even with an otherwise "primitive-looking" name) is correctly treated as non-primitive.

Incorrect

type Props = { title: string; age: number };

const MyComponent = ({ title, age }: Props) => {
  return (
    <h1>
      {title} - {age}
    </h1>
  );
};

type OtherProps = { title: string; onClick: () => void };

const OtherComponent = memo(({ title, onClick }: OtherProps) => {
  return <h1 onClick={onClick}>{title}</h1>;
});

Correct

type Props = { title: string; age: number };

const MyComponent = React.memo(({ title, age }: Props) => {
  return (
    <h1>
      {title} - {age}
    </h1>
  );
});

type OtherProps = { title: string; onClick: () => void };

const OtherComponent = ({ title, onClick }: OtherProps) => {
  return <h1 onClick={onClick}>{title}</h1>;
};

react-memo-primitives/no-unnecessary-memo

Flags a component wrapped in memo(...) or React.memo(...) that doesn't receive any props — memoizing a component with no props buys nothing and adds overhead.

Incorrect

const MyComponent = React.memo(() => {
  return <h1>Static</h1>;
});

Correct

const MyComponent = () => {
  return <h1>Static</h1>;
};

react-memo-primitives/require-memo-displayname

Flags a component wrapped in memo(...) or React.memo(...) that has no displayName assignment — memoized components lose their function name in React DevTools/error boundaries unless displayName is set explicitly.

Incorrect

const MyComponent = React.memo(({ title }) => {
  return <h1>{title}</h1>;
});

Correct

const MyComponent = React.memo(({ title }) => {
  return <h1>{title}</h1>;
});
MyComponent.displayName = "MyComponent";

Installation

npm install eslint-plugin-react-memo-primitives --save-dev

Usage

Flat config (eslint.config.js, ESLint 9+)

import reactMemoPrimitives from "eslint-plugin-react-memo-primitives";

export default [...reactMemoPrimitives.configs["flat/recommended"]];

Or wire the rules up manually:

import reactMemoPrimitives from "eslint-plugin-react-memo-primitives";

export default [
  {
    plugins: { "react-memo-primitives": reactMemoPrimitives },
    rules: {
      "react-memo-primitives/require-memo-primitives": "error",
      "react-memo-primitives/no-unnecessary-memo": "error",
      "react-memo-primitives/require-memo-displayname": "error",
    },
  },
];

Legacy config (.eslintrc)

{
  "plugins": ["react-memo-primitives"],
  "extends": ["plugin:react-memo-primitives/recommended"]
}

Or wire the rules up manually:

{
  "plugins": ["react-memo-primitives"],
  "rules": {
    "react-memo-primitives/require-memo-primitives": "error",
    "react-memo-primitives/no-unnecessary-memo": "error",
    "react-memo-primitives/require-memo-displayname": "error"
  }
}

Compatibility

Requires ESLint 8 or later. ESLint 9's flat config is supported via configs['flat/recommended']. Type-aware primitive-prop detection works when your ESLint config parses TypeScript files with @typescript-eslint/parser (a dependencies entry of this package, so it's installed automatically); projects parsing .tsx with the default parser, or linting plain .jsx, fall back to the naming heuristic described below.

If your ESLint config additionally sets parserOptions.project (standard type-aware ESLint — gives @typescript-eslint/parser a real TypeScript program and checker), require-memo-primitives automatically upgrades to asking the real type checker for every type reference instead of guessing, including types imported from other files or packages. No extra configuration or rule option is needed — it's detected automatically, and everything below in this section only applies when type-aware linting is off.

Limitations

require-memo-primitives reads the destructured parameter's real TS type annotation when one is present (an inline object type, or a reference to a type/interface/enum declared in the same file) to decide whether each prop is primitive. Objects, functions, arrays, tuples, and mapped types are always non-primitive. A named type reference resolved to a local enum is always primitive; resolved to a local object-shaped type it's non-primitive; resolved to a local primitive type alias it's unwrapped and checked recursively. A type that can't be resolved in the current file at all (imported from elsewhere, a generic parameter) falls back to a structural signal: a reference with type arguments (MutableRefObject<T>) is always non-primitive, while a bare reference with none (an imported enum or simple alias, e.g. LocaleType) is trusted as primitive — this is optimistic rather than conservative, since imported object-shaped type aliases with no generics are the uncommon case for prop types.

This whole paragraph's heuristic is skipped entirely when type-aware linting is on (see Compatibility above) — the real checker resolves imported types correctly in both directions, including imported object-shaped aliases with no generics (the case the heuristic above gets wrong). If you're hitting a false positive/negative on an imported type, turning on parserOptions.project is the fix, not working around the heuristic.

When there's no type annotation at all (plain JS/JSX), both rules fall back to a naming heuristic: a prop bound to a lowercase identifier (other than props) is treated as primitive regardless of its actual runtime value — there's no false-positive-proof way to do this without type information, type-aware or not.

All three rules check that memo/React are actually imported from 'react' in the same file, so a same-named identifier imported from elsewhere (import { memo } from 'some-other-lib') isn't mistaken for real memoization. When there's no relevant import in the file at all (e.g. a global React, or an isolated code snippet), all three rules fall back to trusting the name, same as before.

require-memo-displayname only recognizes a Foo.displayName = "..." assignment written as a direct top-level statement in the file — one nested inside another function, conditional, or block isn't detected.

Testing

npm test

Runs all three rules through ESLint's RuleTester (test/*.test.js) via Node's built-in test runner.