react-jsonschema-form で schema を分割して扱うTips

これは「[react-jsonschema-formのカレンダー | Advent Calendar 2023 - Qiita](https://qiita.com/advent-calendar/2023/react-jsonschema-form)」の15日目の記事です。

大きいJSON SchemaをステップUI等で分割して入力させたいときのTipsをまとめてみました。
ステップやカテゴリごとにファイルを分ける方法もありますが、できるだけ汎用的に schema の変更を反映できる実装を意識しています。

## 分割しない場合

まずは分割せずにまとめて表示した際のサンプルです。

```json
{
    "$schema": "http://json-schema.org/draft-07/schema",
    "title": "Test Schema",
    "type": "object",
    "required": [],
    "properties": {
        "category1": {
            "type": "object",
            "properties": {
                "item1": {
                    "type": "string"
                },
                "item2": {
                    "type": "string"
                }
            }
        },
        "category2": {
            "type": "object",
            "properties": {
                "item3": {
                    "type": "string"
                },
                "item4": {
                    "type": "string"
                }
            }
        }
    }
}
```

```tsx
import Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8';
import schema from './schema.json'

function App() {
  return (
    <div style={{marginLeft: '50px'}}>
      <Form schema={schema} validator={validator}/>
    </div>
  )
}

export default App
```

![スクリーンショット 2023-12-15 11.06.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/df53913f-b06c-a935-7304-98da56eb68e6.png)

## schemaの分割

擬似的なステップUIとしてボタンを二つ置き、選択したカテゴリのFormを表示します。
JSON Schema から `schema['properties'][category]` と中身を取り出し、それを RJSForm に渡すだけでカテゴリごとのFormの表示が可能です。

```tsx
  const [category, setCategory] = useState<'category1' | 'category2'>('category1');

  return (
    <div style={{marginLeft: '50px'}}>
      <button onClick={() => setCategory('category1')}>Category1</button>
      <button onClick={() => setCategory('category2')}>Category2</button>
      <Form schema={schema['properties'][category]} validator={validator}/>
    </div>
  )
```

![スクリーンショット 2023-12-15 11.10.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/6f27e5d1-75ea-884a-7376-8a3edb3a6772.png)

![スクリーンショット 2023-12-15 11.10.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/c5baf376-af44-4aaf-4693-8dd8a0ed4903.png)


## definitions を使っている場合

以下のJSON Schema のように definitions を使っている場合は注意が必要です。

```json
{
    "$schema": "http://json-schema.org/draft-07/schema",
    "title": "Test Schema",
    "type": "object",
    "required": [],
    "definitions": {
        "commonItem": {
            "type": "number"
        }
    },
    "properties": {
        "category1": {
            "type": "object",
            "properties": {
                "item1": {
                    "type": "string"
                },
                "item2": {
                    "type": "string"
                },
                "item3": {
                    "$ref": "#/definitions/commonItem"
                }
            }
        },
        "category2": {
            "type": "object",
            "properties": {
                "item4": {
                    "type": "string"
                },
                "item5": {
                    "type": "string"
                },
                "item6": {
                    "$ref": "#/definitions/commonItem"
                }
            }
        }
    }
}
```

ここからカテゴリの Schema を取り出しそのまま RJSForm に渡しても、definitions が参照できずエラーが発生します。

```
Uncaught Error: Could not find a definition for #/definitions/commonItem.
```

この場合は、取り出したカテゴリの Schema に definitions をマージして RJSForm に渡す必要があります。

```tsx
  const [category, setCategory] = useState<'category1' | 'category2'>('category1');

  const targetSchema = {
    ...schema['properties'][category],
    definitions: schema['definitions']
  }

  return (
    <div style={{marginLeft: '50px'}}>
      <button onClick={() => setCategory('category1')}>Category1</button>
      <button onClick={() => setCategory('category2')}>Category2</button>
      <Form schema={targetSchema} validator={validator}/>
    </div>
  )
```

![スクリーンショット 2023-12-15 11.19.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/02ad0ca0-2656-b4f6-8c90-b5a09173989a.png)

![スクリーンショット 2023-12-15 11.19.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/a2cd2044-8719-5d79-0690-7d81aebd8ac3.png)

## uiSchema を使っている場合
分割した　schema　の中で key が重複することがあるかと思います。
例えば以下のような例です。

```json
{
    "$schema": "http://json-schema.org/draft-07/schema",
    "title": "Test Schema",
    "type": "object",
    "required": [],
    "properties": {
        "category1": {
            "type": "object",
            "properties": {
                "item1": {
                    "type": "string"
                },
                "duplicateItem": {
                    "type": "string"
                }
            }
        },
        "category2": {
            "type": "object",
            "properties": {
                "item2": {
                    "type": "string"
                },
                "duplicateItem": {
                    "type": "string"
                }
            }
        }
    }
}
```

この例では `duplicateItem` が重複していますが、 uiSchema を使って表示を調整したいときそのまま `duplicateItem` を指定すると、どちらのカテゴリにも共通の設定がされます。

```tsx
const uiSchema: UiSchema = {
  duplicateItem: {
    "ui:placeholder": "Ex: duplicate item"
  }
}
```

![スクリーンショット 2023-12-15 12.02.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/e0712738-dc74-6c16-cf10-87b86bc0ca2c.png)

![スクリーンショット 2023-12-15 12.02.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/470c5055-d73e-9e7d-bc84-f8a4f9e67995.png)

uiSchema をネストさせカテゴリに応じて中身を取り出し RJSForm に渡すことで表示を切り替えることができます。

```tsx
const uiSchema: UiSchema = {
  category1: {
    duplicateItem: {
      "ui:placeholder": "Ex: duplicate item (c1)"
    }
  },
  category2: {
    duplicateItem: {
      "ui:placeholder": "Ex: duplicate item (c2)"
    }
  }
}

function App() {
  const [category, setCategory] = useState<'category1' | 'category2'>('category1');

  return (
    <div style={{marginLeft: '50px'}}>
      <button onClick={() => setCategory('category1')}>Category1</button>
      <button onClick={() => setCategory('category2')}>Category2</button>
      <Form schema={schema['properties'][category]} validator={validator} uiSchema={uiSchema[category]}/>
    </div>
  )
}
```

![スクリーンショット 2023-12-15 12.06.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/ae525cb5-90ff-74f5-1dd4-85aac5a634a8.png)

![スクリーンショット 2023-12-15 12.06.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/07869c95-3c5f-d391-ef12-5f2b30b8534a.png)

## formRef による Submit の発火

ステップUIなどの複雑なUIと組み合わせたときに、Form の外側から Submit 処理を制御したいことがあります。
その場合は、formRef を利用することで Submit 処理を発火させることができます。

```tsx
  const [category, setCategory] = useState<'category1' | 'category2'>('category1');

  const formRef = useRef<Form>()

  const handleSubmit = () => {
    formRef.current?.submit()
  }

  return (
    <div style={{marginLeft: '50px'}}>
      <button onClick={() => setCategory('category1')}>Category1</button>
      <button onClick={() => setCategory('category2')}>Category2</button>
      <Form ref={formRef} schema={schema['properties'][category]} validator={validator} />
      <button onClick={handleSubmit}>Submit by ref</button>
    </div>
  )
```

![スクリーンショット 2023-12-15 12.19.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96978/c16fdf96-31a9-d83c-bfb7-68592e80252f.png)