DEV Community

Timevolt
Timevolt

Posted on

Breaking Down Complex Problems Like a Dark Souls Boss Fight

The Quest Begins (The “Why”)

Ever stare at a ticket that looks like a novel written in hieroglyphics? I have. Last week I was handed a feature request: “Build a dashboard that lets users filter, sort, and paginate a list of 200 k transactions while preserving real‑time updates from a WebSocket feed.” My brain immediately screamed, “That’s impossible!” I felt like a low‑level knight walking into the dragon’s lair without a shield—overwhelmed, sweating, and already drafting my resignation email in my head.

The truth is, we’ve all been there. The problem looks monolithic, and the instinct is to dive straight into code, hoping brute force will carve a path. Spoiler: it doesn’t. It just leaves us with a tangled mess of state, off‑by‑one errors, and a lingering sense of defeat. I needed a mental framework that turns that dragon into a series of manageable goblins—something I could actually defeat one swing at a time.

The Revelation (The Insight)

The breakthrough came when I remembered a simple mantra my mentor once dropped during a late‑night pair‑programming session: “Solve the smallest possible piece first, then compose.” It sounds obvious, but the magic lies in how you define “smallest possible piece.”

Top coders don’t just chop a problem randomly; they look for independent dimensions—axes of variation that can be tackled separately and later stitched together. Think of a Rubik’s Cube: you don’t try to solve all faces at once; you solve one layer, then the next, using algorithms that don’t disturb what you’ve already fixed.

In our dashboard case, the independent dimensions were:

  1. Data fetching & streaming – getting the raw transaction list and keeping it fresh via WebSocket.
  2. Filtering logic – applying user‑chosen criteria (date range, amount, status).
  3. Sorting – letting the user click column headers to ascend/descend.
  4. Pagination – slicing the filtered‑sorted list into pages.
  5. UI rendering – turning the final slice into rows, handling loading states, etc.

Each dimension could be implemented, tested, and reasoned about in isolation. The “aha!” moment was realizing that the only thing that ties them together is pure function composition: take the output of one step as the input to the next. No hidden globals, no sneaky side‑effects leaking between steps. Once I saw that, the dragon shrank to a series of friendly critters I could handle with unit tests and a clear contract.

Wielding the Power (Code & Examples)

The Struggle (Before)

Here’s what my first attempt looked like—a monolithic component that tried to do everything at once:

// ❌ Before: a tangled mess
function TransactionDashboard({ wsUrl }) {
  const [data, setData] = useState([]);
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState({ field: 'date', dir: 'asc' });
  const [page, setPage] = useState(0);

  useEffect(() => {
    const ws = new WebSocket(wsUrl);
    ws.onmessage = (e) => {
      const newTx = JSON.parse(e.data);
      // 😱 Mutating state directly inside the WebSocket callback
      setData(prev => {
        let updated = [...prev, newTx];
        // Apply filters, sort, and slice all in one go!
        updated = updated.filter(f => matchesFilters(f, filters));
        updated = updated.sort((a, b) => compare(a, b, sort.field, sort.dir));
        const start = page * PAGE_SIZE;
        return updated.slice(start, start + PAGE_SIZE);
      });
    };
    return () => ws.close();
  }, [wsUrl, filters, sort, page]); // <-- dependency hell

  // ... UI rendering omitted
}
Enter fullscreen mode Exit fullscreen mode

Problems?

  • The WebSocket callback does filtering, sorting, and pagination all at once—hard to test.
  • Dependencies on filters, sort, and page cause the effect to re‑run on every UI tweak, tearing down and recreating the WebSocket needlessly.
  • State updates are tangled; a bug in filtering could corrupt the whole pipeline.

The Victory (After)

Now, let’s break it down using the independent‑dimension approach. Each step is a pure function that receives the previous step’s output and returns a new value.

// ✅ Pure helpers – easy to unit test
function applyFilters(txns, filters) {
  return txns.filter(tx => 
    (!filters.startDate || tx.date >= filters.startDate) &&
    (!filters.endDate   || tx.date <= filters.endDate) &&
    (!filters.minAmount || tx.amount >= filters.minAmount) &&
    (!filters.maxAmount || tx.amount <= filters.maxAmount) &&
    (!filters.status    || tx.status === filters.status)
  );
}

function sortTxns(txns, { field, dir }) {
  return [...txns].sort((a, b) => {
    if (a[field] < b[field]) return dir === 'asc' ? -1 : 1;
    if (a[field] > b[field]) return dir === 'asc' ? 1 : -1;
    return 0;
  });
}

function paginate(txns, page, pageSize = 50) {
  const start = page * pageSize;
  return txns.slice(start, start + pageSize);
}

// ----------------------------
// The orchestrating component
// ----------------------------
import { useEffect, useState, useMemo } from 'react';

function TransactionDashboard({ wsUrl }) {
  const [rawData, setRawData] = useState([]);
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState({ field: 'date', dir: 'asc' });
  const [page, setPage] = useState(0);

  // 1️⃣ Isolated data source – only concerns itself with the WS stream
  useEffect(() => {
    const ws = new WebSocket(wsUrl);
    ws.onmessage = e => {
      const tx = JSON.parse(e.data);
      setRawData(prev => [...prev, tx]); // just append, no side‑effects
    };
    return () => ws.close();
  }, [wsUrl]); // ← stable, no UI deps

  // 2️⃣ Pure transformation pipeline – each step is memoized
  const filtered = useMemo(() => applyFilters(rawData, filters), [rawData, filters]);
  const sorted   = useMemo(() => sortTxns(filtered, sort), [filtered, sort]);
  const pageData = useMemo(() => paginate(sorted, page), [sorted, page]);

  // 3️⃣ UI stays dumb – it just renders what it receives
  return (
    <div>
      <FilterPanel onChange={setFilters} />
      <SortBar onChange={setSort} />
      <Paginator page={page} total={sorted.length} onPageChange={setPage} />
      <TransactionTable data={pageData} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What changed?

  • The WebSocket effect now only appends raw messages. No filtering, sorting, or slicing lives there.
  • Each transformation (applyFilters, sortTxns, paginate) is a pure function—trivial to test with Jest.
  • useMemo ensures we only recompute when its inputs actually change, preventing wasted work.
  • UI components are decoupled; they simply trigger state setters, letting the pipeline do the heavy lifting.

Common Traps to Avoid

Trap Why it hurts Fix
Putting side‑effects in derived calculations (e.g., mutating state inside a useMemo) Breaks React’s reconciliation, leads to stale UI and infinite loops. Keep derivations pure; side‑effects belong in effects or event handlers.
Over‑memoizing (wrapping every tiny calculation in useMemo) Adds noise, can actually slow things down if dependencies are cheap. Memoise only when the computation is expensive or when it’s used as a dependency of another effect.
Ignoring ordering (applying pagination before filtering) Returns the wrong slice—users see missing data. Always respect the logical flow: fetch → filter → sort → paginate → render.

Why This New Power Matters

Adopting this “divide‑and‑compose” mindset turned that terrifying dashboard ticket into a set of bite‑sized challenges I could knock out before lunch. The benefits ripple outward:

  • Testability – each pure function gets its own unit test suite; no need to spin up a WebSocket mock just to verify a filter.
  • Maintainability – if the product team asks for a new filter dimension, I drop it into applyFilters and the rest of the pipeline stays untouched.
  • Performance – memoization prevents needless recalculations, and the WebSocket stays alive unless the URL actually changes.
  • Confidence – when I push a change, I know I haven’t accidentally broken sorting because I’m only touching one isolated piece.

In short, treating a complex problem as a composition of independent, pure steps is like discovering a hidden shortcut in a boss fight: you still have to swing your sword, but you now know exactly where to strike for maximum impact.

Your Turn

Pick a feature you’ve been dreading—a report generator, a multi‑step wizard, a real‑time chat filter—and lay out its independent dimensions on a piece of paper (or a digital note). Write one pure function for each dimension, wire them together with useMemo or similar, and watch the monster shrink.

What’s the first dimension you’ll isolate? Drop a comment below and let’s celebrate each other’s victories!


Happy coding, and may your bugs be few and your composability high.

Top comments (0)