Lanning Blog

Improving React code with checkr

useState as immutable warning message

Many common bugs and coding guidelines can be enforced by static analysis. Unfortunately, they rarely are. Writing a custom ESLint rule has a steep learning curve and requires various ceremony. Conversely, checkr only requires knowing regex.

Here's some React (and plain JavaScript) checkr snippets that I've found useful for preventing bugs and sharing coding guidelines. These examples are not perfect, nor as robust as Abstract Syntax Tree parsing. But in most cases, they get the job done and took under 20 minutes to write.

1. Mismatched useState names.

// ✔️ Good.
const [viewportWidth, setViewportWidth] = useState({});

// ❌ Inconsistent with rest of project.
const [viewportWidth, setViewport] = useState({});

From a high level, we grab all the useState matches and then loop through to verify that the names match the project convention. Again, the regex isn't perfect, but it catches most cases.

// Add to your checkr.js file.
function mismatchedUseStateNames({ fileExtension, fileContents }, underline) {
  if (fileExtension !== "js" && fileExtension !== "jsx") {
    return;
  }

  const useState = /const \[(.+), (.+)\] = useState\([\s\S]*?\);/g;
  let useStateMatches;
  while ((useStateMatches = useState.exec(fileContents)) != null) {
    const line = useStateMatches[0];
    const variableName = useStateMatches[1];
    const setVariableName = useStateMatches[2].slice("set".length);

    // Doesn't validate casing, but not a big deal in practice.
    if (variableName.toLowerCase() !== setVariableName.toLowerCase()) {
      const upperCaseVariableName =
        variableName.charAt(0).toUpperCase() + variableName.slice(1);
      underline(
        line,
        `useState variable name '${variableName}' should follow 'set${upperCaseVariableName}' convention.`,
        "warn"
      );
    }
  }
}

2. Modifying state variables directly. This is a common React bug.

const [user, setUser] = useState({});

// ✔️ Good.
setUser({ userName: "Foo" });

// ❌ Likely a bug.
user.userName = "Foo";

Again, we grab all of the useState matches and check that they (or their sub properties) are not directly assigned.

// Add to your checkr.js file.
function noModifyingUseStateDirectly(
  { fileExtension, fileContents },
  underline
) {
  if (fileExtension !== "js" && fileExtension !== "jsx") {
    return;
  }

  const useState = /const \[(.+), .+\] = useState\([\s\S]*?\);/g;
  let useStateMatches;
  while ((useStateMatches = useState.exec(fileContents)) != null) {
    const variableName = useStateMatches[1];

    const reassigningVariable = new RegExp(
      `${variableName}(\.[a-zA-Z0-9]+)* = .+;`,
      "g"
    );
    const upperCaseVariableName =
      variableName.charAt(0).toUpperCase() + variableName.slice(1);

    underline(
      reassigningVariable,
      `Avoid assigning state variable directly. Prefer set${upperCaseVariableName}(...).`,
      "error"
    );
  }
}

3. Importing from a sibling or parent file through index.js instead of directly. This can cause cyclic dependencies and makes code harder to follow.

// ✔️ Good.
import { FormHeader } from "./formHeader";

// ❌ Confusing and potential for cyclic dependency issues.
import { FormHeader } from "./";

Here we're just looking for the sibling or parent index.js import pattern in regex. Very straightforward and easy to read. Note, this only checks for imports using a single quote, eg from './foobar' not double quotes such as from "./foobar". Enforcing single quotes or double quotes should be done with an ESLint rule and varies project to project. It will also not catch import './'; but could easily be extended to do so.

// Add to your checkr.js file.
function avoidParentAndSiblingIndexImports({ fileExtension }, underline) {
  if (fileExtension !== "js" && fileExtension !== "jsx") {
    return;
  }

  const siblingImport = /from '\.\/';/g;
  underline(
    siblingImport,
    "Avoid importing from sibling modules index.js",
    "info"
  );

  const parentImports = /from '(\.\.\/)+';/g;
  underline(
    parentImports,
    "Avoid importing from parent modules index.js",
    "info"
  );
}

Conclusions