「useMemoって何?columnsって何?」——Reactテーブルでハマらないための超やさしい解説

※当ブログでは商品・サービスのリンク先にプロモーションを含みます。ご了承ください。

「追加を押したら addMode が true のはずなのに、なぜか false に戻ってる…」
この“あるある”の正体は、だいたい useMemo の依存配列列定義(columns)の中で読む stateにあります。この記事では、まず用語をサクッと整理し、その後「どこで何を入れればハマらないか」をシンプルなサンプルコードで解説します。


スポンサーリンク

useMemo は「依存値が変わった時だけ再計算して、それ以外は前回の結果を再利用」するフックです。

  • 毎回新しいオブジェクト/配列を作ると子コンポーネントが無駄に再レンダリングしがち → useMemo で安定化
  • ただし!**依存配列に入れてない値は“古いまま”**になります(=stale closure
const [count, setCount] = useState(0);

// ❌ countを中で使っているのに、依存配列が []
const expensive = useMemo(() => {
  console.log('計算:count =', count); // ← ここがずっと古いcountを見ることに
  return count * 2;
}, []); // ← 依存が空

<button onClick={() => setCount(c => c + 1)}>+1</button>
const [count, setCount] = useState(0);

// ✅ 依存に count を入れる
const expensive = useMemo(() => {
  console.log('計算:count =', count);
  return count * 2;
}, [count]);

@tanstack/react-table などで使う 列定義の配列columns
1列ごとに「ヘッダはこう表示」「セルはこう描画」「ソートできる?」などの振る舞いを書きます。

import React, { useMemo, useState } from 'react';
import { useReactTable, getCoreRowModel, flexRender, createColumnHelper } from '@tanstack/react-table';

type Todo = { id: number; title: string; done: boolean };

export default function TodoTable() {
  const [todos] = useState<Todo[]>([
    { id: 1, title: '牛乳を買う', done: false },
    { id: 2, title: '本を読む',   done: true  },
  ]);

  const columnHelper = createColumnHelper<Todo>();

  // ✅ columnsはuseMemoで安定化(今回はstate参照がないので依存配列は[]でOK)
  const columns = useMemo(() => [
    columnHelper.accessor('title', {
      header: 'タイトル',
      cell: info => info.getValue(),
    }),
    columnHelper.accessor('done', {
      header: '完了?',
      cell: info => (info.getValue() ? 'はい' : 'いいえ'),
    }),
  ], []);

  const table = useReactTable({
    data: todos,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(hg => (
          <tr key={hg.id}>
            {hg.headers.map(h => (
              <th key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(r => (
          <tr key={r.id}>
            {r.getVisibleCells().map(c => (
              <td key={c.id}>{flexRender(c.column.columnDef.cell, c.getContext())}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

ポイント:この例では columns の中で外部 state を参照していないので、useMemo の依存配列は [] でもOK。


「追加モード(addMode)の時は編集ボタンを無効にする」みたいなUI分岐列定義の中に書くと、列定義が古いstateを握りっぱなしになりがちです。

const [addMode, setAddMode] = useState(false);

const columns = useMemo(() => [
  {
    id: 'actions',
    header: '操作',
    cell: ({ row }) => (
      <button
        // ❌ ここでaddModeを読んでいるのに…
        disabled={addMode} 
        onClick={() => {
          if (addMode) return;  // ここもaddModeを参照
          alert(`編集: ${row.original.title}`);
        }}
      >
        編集
      </button>
    )
  }
], []); // ❌ 依存配列にaddModeがない

この場合、**ボタンは「columnsを作った時点の addMode」**を見続けます。
= 追加モードに入っても 古い false を読んで編集できちゃう → 別の処理で setAddMode(false) が実行…という事故が起きます。

const [addMode, setAddMode] = useState(false);

const columns = useMemo(() => [
  {
    id: 'actions',
    header: '操作',
    cell: ({ row }) => (
      <button
        type="button"
        disabled={addMode} // ✅ 最新の値が入る
        onClick={() => {
          if (addMode) return;
          alert(`編集: ${row.original.title}`);
        }}
      >
        編集
      </button>
    ),
  }
], [addMode]); // ✅ addModeを依存に入れる

「追加中は既存行は通常UI」「通常編集は編集中の行だけ“保存/戻る”」という行単位の切替が鉄板です。
ページ全体の editMode だけで“保存/戻る”にしないのがコツ。

type RowData = { id: number; title: string };
const [addMode, setAddMode] = useState(false);
const [editMode, setEditMode] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);

const columns = useMemo(() => [
  {
    id: 'title',
    header: 'タイトル',
    cell: (info: any) => info.row.original.title,
  },
  {
    id: 'actions',
    header: '操作',
    cell: ({ row }: any) => {
      const r = row.original as RowData;

      // ✅ 行単位で判定:追加モードなら既存行は通常UI
      const isEditingThisRow = !addMode && editMode && editingId === r.id;

      if (isEditingThisRow) {
        return (
          <>
            <button type="button" onClick={() => {/* 保存 */}}>保存</button>
            <button
              type="button"
              onClick={() => { setEditMode(false); setEditingId(null); }}
            >
              戻る
            </button>
          </>
        );
      }

      return (
        <>
          <button
            type="button"
            disabled={addMode || (editMode && editingId !== r.id)}
            onClick={() => {
              if (addMode) return; // 追加中は編集に入らない
              setEditMode(true);
              setEditingId(r.id);
            }}
          >
            編集
          </button>
          <button
            type="button"
            disabled={addMode || (editMode && editingId !== r.id)}
            onClick={() => {/* 削除 */}}
          >
            削除
          </button>
        </>
      );
    },
  },
], [addMode, editMode, editingId]);

さらに type="button" を付けておくと、フォーム内でも意図せぬ submitを防げます。
これで handleCancelEdit() が勝手に走って setAddMode(false) される事故も回避。


  • columnsuseMemo 依存配列に、中で参照しているすべての state/propsaddMode, editMode, editingId など)を入れているか?
  • “編集中UI”は行単位で切り替えているか?(ページ全体の editMode だけで切り替えない)
  • ボタンは type="button" を付け、フォーム submit 暴発を防いでいるか?
  • 「追加モード中に既存行の編集を無効化する」分岐がUIとロジックの両方に入っているか?(UIだけ無効化してもイベントが通ると事故る)

  • useMemo は“結果のキャッシュ”。依存に入れない値は古いままstale closure
  • columns はテーブルの設計図。中で state を読んだら必ず依存に入れる
  • “編集中UI”は行単位で出し分け、追加モード中は既存行を編集不可に。
  • type="button" と早期 return副作用の入口を塞ぐ

この4点を守れば、「追加ボタン押下後に addMode が勝手に false になって見える」系のトラブルはグッと減ります。シンプルに、堅実に、いきましょう。

スポンサーリンク
プログラミング
シェアする
こーんをフォローする

コメント

タイトルとURLをコピーしました