"Hey, I take pride in MY organized mess!" (source)


Problem

Your utils.ts has become 200+ lines long 😨🤦‍♂️

Triage

“Wait, how did we end up here?”

  • As we add more functionality to our React components, our files get bigger (of course they do)
  • A standard strategy to combat growing file sizes is to move non-JSX logic into a companion utils.ts that sits in the same folder as the initial React component.
  • Over a couple of sprints, this utils.ts gets stuffed with more and more functions until we get an (extremely) bloated utils.ts. Foie gras, anyone? 🦆


“Surely it’s not that bad, right? 💁‍♀️”



"This is fine"
Illustration by KC Green


No, it’s bad - Really bad stuff. Here’s why:

  • Increased entropy: Chance of cross-context function calls (and we don’t have import/require syntax to rely on as hints)
  • Increased fatigue: Extremely fatiguing for developers to reason about multiple contexts, even if code does not cross contexts
  • Increased risk of errors: High risk of human error due to high volume of logic and multiple contexts within same file
  • Poor discoverability: Context information is lost under generic utils.ts filename
  • Large test suite: Tests for all contexts within utils.ts would usually sit under utils.test.ts (unless you’ve already taken the effort to break it down into __test__/context_xyz.test.ts)
  • Poor cohesion: Low overall cohesion (even if code is grouped context-wise), since a single file contains code which encompasses multiple contexts

Solution

Steps:

  • Refactor utils.ts into smaller files within /utils folder
  • Use meaningful names for each file to indicate context
  • Colocate associated helper functions to specific contexts
  • Export utils as a group via utils/index.ts

Example code: refactor-your-utils

Before:

utils.ts
import { MyItem, MyItemPriceCurrency } from "../common/typedefs";

// item price utils
const sortItemsByPriceAmountAscending = (items: MyItem[]): MyItem[] =>
  items.sort((a, b) => a.price.amount - b.price.amount);

export const getCheapestItem = (items: MyItem[]): MyItem => {
  return sortItemsByPriceAmountAscending(items)[0];
};

export const getMostExpensiveItem = (items: MyItem[]): MyItem => {
  return sortItemsByPriceAmountAscending(items).reverse()[0];
};

// item currency utils
export const filterBySgdItem = (items: MyItem[]) =>
  items.filter(filterByItem("SGD"));

export const filterByJpyItem = (items: MyItem[]) =>
  items.filter(filterByItem("JPY"));

const filterByItem = (targetCurrency: MyItemPriceCurrency) => (item: MyItem) =>
  item.price.currency === targetCurrency;

// item component utils
export const makeItemComponentKey = (item: MyItem) => item.id;
utils.test.ts
import { MyItem } from "../common/typedefs";
import {
  filterByJpyItem,
  filterBySgdItem,
  getCheapestItem,
  getMostExpensiveItem,
  makeItemComponentKey,
} from "./utils";

describe("utils", () => {
  const appleItem: MyItem = {
    id: "abcd",
    title: "Apple",
    price: { amount: 12.3, currency: "SGD" },
  };
  const pearItem: MyItem = {
    id: "efgh",
    title: "Pear",
    price: { amount: 10.23, currency: "SGD" },
  };
  const cornItem: MyItem = {
    id: "ijkl",
    title: "Corn",
    price: { amount: 30.01, currency: "JPY" },
  };
  const mockItems: MyItem[] = [appleItem, pearItem, cornItem];

  it("gets cheapest item", () => {
    expect(getCheapestItem(mockItems)).toEqual(pearItem);
  });

  it("gets most expensive item", () => {
    expect(getMostExpensiveItem(mockItems)).toEqual(cornItem);
  });

  it("filters by sgd items", () => {
    expect(filterBySgdItem(mockItems)).toEqual([appleItem, pearItem]);
  });

  it("filters by jpy items", () => {
    expect(filterByJpyItem(mockItems)).toEqual([cornItem]);
  });

  it("makes item component key", () => {
    expect(makeItemComponentKey(appleItem)).toEqual("abcd");
  });
});


After:

utils/itemFilterUtils.ts
import { MyItem, MyItemPriceCurrency } from "../../common/typedefs";

export const filterBySgdItem = (items: MyItem[]) =>
  items.filter(filterByItem("SGD"));

export const filterByJpyItem = (items: MyItem[]) =>
  items.filter(filterByItem("JPY"));

const filterByItem = (targetCurrency: MyItemPriceCurrency) => (item: MyItem) =>
  item.price.currency === targetCurrency;
utils/itemSortUtils.ts
import { MyItem } from "../../common/typedefs";

const sortItemsByPriceAmountAscending = (items: MyItem[]): MyItem[] =>
  items.sort((a, b) => a.price.amount - b.price.amount);

export const getCheapestItem = (items: MyItem[]): MyItem => {
  return sortItemsByPriceAmountAscending(items)[0];
};

export const getMostExpensiveItem = (items: MyItem[]): MyItem => {
  return sortItemsByPriceAmountAscending(items).reverse()[0];
};
utils/itemRenderUtils.ts
import { MyItem } from "../../common/typedefs";

export const makeItemComponentKey = (item: MyItem) => item.id;
utils/index.ts
export * from "./itemFilterUtils";
export * from "./itemRenderUtils";
export * from "./itemSortUtils";
utils/__test__/itemFilterUtils.test.ts
import { filterByJpyItem, filterBySgdItem } from "../itemFilterUtils";
import { testMocks } from "./testMocks";

describe("itemFilterUtils", () => {
  const { mockItems, appleItem, pearItem, cornItem } = testMocks;

  it("filters by sgd items", () => {
    expect(filterBySgdItem(mockItems)).toEqual([appleItem, pearItem]);
  });

  it("filters by jpy items", () => {
    expect(filterByJpyItem(mockItems)).toEqual([cornItem]);
  });
});
utils/__test__/itemSortUtils.test.ts
import { getCheapestItem, getMostExpensiveItem } from "../itemSortUtils";
import { testMocks } from "./testMocks";

describe("itemSortUtils", () => {
  const { mockItems, pearItem, cornItem } = testMocks;

  it("gets cheapest item", () => {
    expect(getCheapestItem(mockItems)).toEqual(pearItem);
  });

  it("gets most expensive item", () => {
    expect(getMostExpensiveItem(mockItems)).toEqual(cornItem);
  });
});
utils/__test__/itemRenderUtils.test.ts
import { makeItemComponentKey } from "../itemRenderUtils";
import { testMocks } from "./testMocks";

describe("itemRenderUtils", () => {
  const { appleItem } = testMocks;

  it("makes item component key", () => {
    expect(makeItemComponentKey(appleItem)).toEqual("abcd");
  });
});


Summary

Here’s why you should refactor (read: kill) all your utils.ts:

  • Low entropy: Colocation of context-specific core and helper functions help to reduce cross-context function calls
  • Less fatigue: Single-context files are far easier to reason about
  • Less risk of errors: Narrower contexts reduces risk of human error
  • Good discoverability: Well-named files help with overall discoverability of project
  • Small test suites: Smaller test suites are easier to maintain
  • Good cohesion: High file-level cohesion, since a single file contains only code that is specific to a single context

As always, keep your code ⭐️ sparkling clean! ⭐️  Peace out ✌️