金曜日, 9月 26, 2025
金曜日, 9月 26, 2025
- Advertisment -
ホームニューステックニュースTanStack Form v1.21.3 配列削除で falsy 値が undefined に化けるバグの解析と回避策

TanStack Form v1.21.3 配列削除で falsy 値が undefined に化けるバグの解析と回避策


配列フィールドを扱っているときに「空文字が消えた?」「0 が undefined になった?」と違和感を覚えたことはありませんか?
私も最初は自分のコードを疑いましたが、調べてみるとこれは TanStack Form 側の既知バグ でした(Issue #1439)。

この記事では内部処理を追跡しながら、なぜ falsy 値が undefined に化けるのか を解説し、修正が入るまでの回避策を整理します。

TL;DR

  • 配列要素を削除すると '' / 0 / false / nullundefined に変換される
  • 原因は FieldApi.update 内の setValue(val || defaultValue)
  • PR #1440 で一度修正が提案されたが未解決
  • 回避策は以下の3つ
    • defaultValue を明示する
    • useEffectundefined を補正する
    • Zod で正規化する

事象

削除操作を行うと、後続要素の値がシフトされる際に falsy 値が undefined に化けます。

削除前

items[0] = { id: "id_1", value: "will be deleted" }
items[1] = { id: "id_2", value: "" }  

削除後(❌ バグ発生)

items[0] = { id: "id_2", value: undefined }  

0falsenull も同様に消えてしまいます。

再現コード
import { useForm } from '@tanstack/react-form'

function App() {
  const form = useForm({
    defaultValues: {
      items: []
    }
  })

  return (
    form.Field name="items" mode="array">
      {(field) => (
        >
          button onClick={() => field.pushValue({ id: Date.now(), value: '' })}>
            ""を追加
          button>

          {field.state.value.map((item, index) => (
            div key={item.id}>  {}
              form.Field name={`items[${index}].value`}>
                {(subField) => (
                  >
                    span>
                      Value: {subField.state.value === undefined ? '❌ undefined' : `"${subField.state.value}"`}
                    span>
                    button onClick={() => field.removeValue(index)}>削除button>
                  >
                )}
              form.Field>
            div>
          ))}
        >
      )}
    form.Field>
  )
}

内部処理の流れ

この現象は「削除」から「再レンダリング」までの間に発生します。シーケンス図で見ると以下のようになります。

では、実際のコードと処理の流れを追ってみましょう。

1. 削除処理を実行

form.Field name="items" mode="array">
  {(field) => (
    button onClick={() => field.removeValue(0)}>削除button>
  )}
form.Field>

2. FieldApi.removeValue の実行

https://github.com/TanStack/form/blob/v1.21.3/packages/form-core/src/FieldApi.ts#L1468-L1473

ここで実際の削除処理は FormApi に委譲されます。

3. FormApi.removeFieldValue の実行

https://github.com/TanStack/form/blob/v1.21.3/packages/form-core/src/FormApi.ts#L2341-L2373

  • 配列から該当要素を削除し、インデックスを詰め直す
  • メタ情報(エラー、dirty状態など)をシフトする
  • 最後のインデックスのフィールドを完全削除(これがundefinedの原因)
  • 再検証を実行する

4. useField による再レンダリング

https://github.com/TanStack/form/blob/v1.21.3/packages/react-form/src/useField.tsx#L228-L230

削除によってフィールド名が items[1].value → items[0].value のように変更されるため、update が呼ばれます。

5. FieldApi.update(バグ発生箇所)

https://github.com/TanStack/form/blob/v1.21.3/packages/form-core/src/FieldApi.ts#L1318-L1334

update = (opts: FieldApiOptions) => {
  const nameHasChanged = this.name !== opts.name
  this.name = opts.name

  
  if ((this.state.value as unknown) === undefined) {
    const formDefault = getBy(opts.form.options.defaultValues, opts.name)
    const defaultValue = (opts.defaultValue as unknown) ?? formDefault

    if (nameHasChanged) {
      
      this.setValue((val) => (val as unknown) || defaultValue, { dontUpdateMeta: true })
    }
  }
}

バグが発生する条件:

  1. (this.state.value as unknown) === undefined(現在の値が未定義)
  2. nameHasChanged === true(フィールド名が変更された)

削除時は両方の条件を満たすため、setValueupdater 関数で ""、 0、false、null といった falsy 値が || によって undefined に置き換えられてしまう。

根本原因

直接の原因は val || defaultValue ですが、より本質的には「配列削除時にフィールド名が変わる設計」と「フィールドの初期化処理」の組み合わせにあります。

なぜ両条件を満たすのか:

  1. 配列削除により items[1]items[0] にインデックスがシフト
  2. 重要: FormApi.removeFieldValue で最後のインデックス(items[1])の全フィールドが deleteField により完全削除される

    • Storeから値が消去される(deleteBy(newState.values, f)
    • フィールド情報も削除される(delete this.fieldInfo[f]
  3. React の再レンダリングで、FieldApiが新しい名前で更新される
  4. useField が新しい名前(items[0].value)で fieldApi.update を呼ぶ
  5. この時点で this.state.valueundefined(Storeから値が削除されているため)
  6. nameHasChangedtrue(名前が items[1].valueitems[0].value に変化)
  7. 両条件を満たすため、setValue のupdater関数が実行される
  8. Storeから値を取得しようとするが、|| 演算子により falsy 値が undefined に変換される

メンテナーも PR #1440 で「本来はStoreレベルで修正すべき」とコメントしていました。

当面の回避策(どれを選ぶ?)

1. defaultValue を設定する(安全だが初期値と競合しやすい)

form.Field
  name={`items[${index}].value`}
  defaultValue=""
>
  {(subField) => (
  )}
form.Field>

2. useEffect で補正する

const field = useField({ form, name })
useEffect(() => {
  if (field.state.value === undefined) {
    field.setValue('')
  }
}, [field.state.value])
  • ✅ 将来公式で修正されたら削除しやすい
  • ⚠️ 各コンポーネントで記述が必要

3. Zod で正規化する

z.object({
  value: z.string().default("").or(z.undefined()).transform(v => v ?? "")
})
  • ✅ 入力値を統一的に正規化できる
  • ⚠️ スキーマが大量にある場合はメンテが大変

プロダクトでの選択

私は useEffect で補正 を選びました。

理由は:

  1. プロダクト内にスキーマが大量にあるため、Zod 側で正規化すると影響範囲が大きすぎる
  2. defaultValuedefaultValues を上書きしてしまうため採用しづらい

結果として、useEffect による補正が最も影響範囲を小さく抑えられ、将来的な修正にも対応しやすいと判断しました。

おわりに

今回のバグ調査を通じて、TanStack Form の内部構造(FieldApi、FormApi、Store の3層構造)や、配列フィールドの管理方法について深く理解する良い機会になりました。

時間があるときに、この問題を解決する PR を作成してみるのも面白そうです。

この記事が、同じ問題に遭遇した方の参考になれば幸いです。

関連リンク



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -