Back to Blog

Language: English

Moving Decisions from Component to Model

AnnotationView carried decisions like claimable checks and tutorial display logic alongside its rendering; moving them into pure functions on the model side, spread across nearly 40 PRs, made everything far easier to test.

Alongside tidying up the feature structure (flatten features into a single tier and they balloon again), I also carved out the insides of AnnotationView.

What I did was shift the decisions a component was holding onto the model side:

  • deciding whether a task is claimable
  • deciding whether a status is terminal
  • classifying fetch errors
  • the plan that assembles the next task’s input
  • the plan for progress display
  • deciding whether to show the tutorial
  • shortcut lock determination
  • restoring selection from history
  • syncing with localStorage

Using AnnotationView, the biggest one, as the example, let’s look at the before and after shapes.

// Before: 判断が component の中にある(イメージ)
export function AnnotationView({ task }: Props) {
  // claimable かどうかの判定を、描画と同じ場所で行っている
  const canClaim = judgeClaimable(task);
  // tutorial を出すかどうかの判定も、ここにある
  const showTutorial = judgeTutorial(task);

  return <TaskPane task={task} canClaim={canClaim} showTutorial={showTutorial} />;
}
// After: 判断は model の入出力関数に寄り、component は結果を受け取るだけ(イメージ)
const view = buildAnnotationViewState(task, history);
// view.canClaim / view.showTutorial は、純粋な戻り値として取り出せる

return <TaskPane {...view} />;

Decisions live in model functions, and the component simply receives the results.

When these decisions are mixed into a component, state, rendering, and logic cohabit the same function. Testing leaves you no option but to render the component.

Push them to the model side, and you can verify them as pure input-output functions.

// レンダリングせずに、判定だけを確かめられる(イメージ)
expect(buildAnnotationViewState(claimableTask, emptyHistory).canClaim).toBe(true);

I moved one decision per PR, splitting the work into nearly 40 changes. Because the job changes nothing but placement without touching behavior, batching it makes the intent of each diff unreadable during review.