【React Table V8 & TypeScript 】 ユーザー入力でセルを書き換える


こんにちはフロントエンドエンジニアの茶木です。
最近は学習用の Next.js の ローカルの SandBox を育てています。

今回は React Table による動的なデータの更新をどうするか書いていきます。

また、シリーズの途中で React Table を V7 → V8にアップデートしていますので、過去記事との差分を比較するときは参考にしてください

現状確認

過去記事でセルの装飾と加工を行っています。上記記事は React Table V7 で紹介しており、以下は テーブルコンポーネントの呼び出し箇所を V8に書き直したものです。

const columns = [
  columnHelper.accessor("id", {
    header: "id",
    cell: IdCell,
  }),
  columnHelper.accessor("updatedTimestamp", {
    header: "更新日",
    cell: DateCell,
  }),
  columnHelper.accessor("name", {
    header: "名前",
  }),
  columnHelper.accessor("age", {
    header: "年齢",
  }),
];

export default function V8() {
  return (
    <PageBase title={"V8"}>
      <div>
        <h1 className={styles.title}>V8</h1>
        <Table<MatchResult> columns={columns} api={userApi} />
      </div>
    </PageBase>
  );
}

ここでは、名前をユーザー入力によって変えられるようにします。

Cell 内の TextField

import { ReactElement } from "react";
import TextField from "@mui/material/TextField";
import { styled } from "@mui/material/styles";
import { BaseRow } from "../../../difinitions/table";
import { CellContext } from "@tanstack/react-table";

const Text = styled(TextField)({
  fontWeight: "bold",
});

type Props<T extends BaseRow> = CellContext<T, string>;

export const NameCell = <T extends BaseRow>(props: Props<T>): ReactElement => {
  return <Text defaultValue={props.getValue()} />;
};

テーブルセルにテキストフィールドを入れること自体は簡単です。ここでは MUI を使っています。

PutApiと連携する

変更された「名前」は TextField コンポーネントが保持しているだけなので、リロードすれば失われてしまいます。もちろん DB上の情報も更新されていません。

そこで、TextField の 変更を検知して putApi をコールし DB を書き換えたあと、getApi を再度コールし、テーブルの更新を行えるようにします。

これには 2つの仕組みが必要です

  • テーブルのリロードメソッドを作り NameCell まで届ける
  • TextField の変更を監視して、putApi の後、 リロードする

テーブルのリロード

useGetApi

ここから一苦労なのですが、 useGetApi というフックを用意します。

import { AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";
export type ApiRes<Res> = Promise<AxiosResponse<Res>>;
export type UseGetApiRes<Res> = [Res | undefined, () => void];

export const useGetApi = <Res>(
  getApi: () => ApiRes<Res>
): UseGetApiRes<Res> => {
  const [data, setData] = useState<Res>();
  const reload = useCallback(() => {
    getApi().then((res) => {
      setData(res.data);
    });
    console.log("reload");
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  useEffect(() => {
    reload();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return [data, reload];
};

api を渡すと tableDatareload を作ります。reload を呼ぶと、api がコールされ、tableData が再度生成されます。

TableMeta

この reloaduseReactTablemeta に渡しておきます。 metaCell コンポーネント内でアクセスできます。 declare を書いて置くことで vscode が meta.reload にアクセスしたときに認識します。

declare module "@tanstack/table-core" {
  interface TableMeta<TData extends RowData> {
    reload: () => void;
  }
}

const table = useReactTable<R>({
  data: rows,
  columns,
  getCoreRowModel: getCoreRowModel(),
  meta: {
    reload,
  },
});

データの更新

総決算です。TextField は制御コンポーネントに変えてあります。

ユーザーの入力により onChange から setValue がコールされ TextField の表示が変更されます。続いて onBlur が呼ばれます。これを入力完了のトリガーとみなして、putApi (DBの名前の値を更新する想定)をコールし、その後、テーブルリロードを行います。

テーブルのリロードが発生すると initialValue の値が更新され useStateObserve の働きで value も更新され、TextField の値に最新の DBの値が表示されます。

export const NameCell = <T extends BaseRow>(props: Props<T>): ReactElement => {
  const initialValue = props.getValue();
  const [value, setValue] = useStateObserve(initialValue, [initialValue]);

  const onBlur = useCallback(async() => {
    await putApi(props.row.id, { name: value });
    props.table.options.meta?.reload();
  }, [value, props.row.id, props.table.options.meta]);
  return (
    <Text
      value={value}
      onBlur={onBlur}
      onChange={(e) => setValue(e.target.value)}
    />
  );
};

解説

meta.reload

リロードメソッドは、props.table.options.meta?.reload として参照できます。

useReactTablemeta に指定したもの各セルで参照できます。ここでは更新関数 reload を渡していました。

useStateObserve

useState(initialValue) は 初回以降は initialValue の変更を感知しません。感知するのは setState によって value を変更したときだけです。useStateObserve は、setState による value の変更に加え、initialValue の変更を感知し、value を書き換えます。

これにより、ユーザー入力と、API による更新の両方で再描画が走ります。

useStateObserve の実装内容については以下の記事をご参照ください。

Gaji-Laboでは、 Next.js 経験が豊富なフロントエンドエンジニアを募集しています

弊社では Next.js の知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違う Gaji-Labo を味わいに来ませんか?

Next.js の設計・実装を得意とするフロントエンドエンジニア募集要項

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!


投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。