ReactでHelloWorldしてから、ちょっとずつ足していく #5

ReactでHelloWorldしてから、ちょっとずつ足していく [#1](https://qiita.com/sato_c/items/e8c41eee6cbcf45355ab) [#2](https://qiita.com/sato_c/items/95ef771e2183f540e9ec) [#3](https://qiita.com/sato_c/items/3f825c6b3580a5f6946b)
[#4](https://qiita.com/sato_c/items/2db853d2812e36a4af4c)

## はじめに

今回は前回に引き続き、
 - stateのリフトアップ
で、うまく行かなかった部分を修正していきます。

### 削除用のチェックボックスの処理

前回、削除する人たちの名前の前にチェックボックスを付けました。
これがチェックされている人は削除ボタンを押したら削除されるようにしようと思っています。

しかし、

TypeError: Cannot read property 'deleteList' of undefined

が出てしまい、うまく動きません。

Objectの中身を見る方法を調べたら、util.inspectならば見られるとわかりましたので早速やってみます。

```ts
  handleDeleteList(user_name: StringT, listset:boolean, deletelist: StringT[]) {
//    alert(user_name + "/" + listset)
     alert(util.inspect(this,false,null))

    var i;
    ... 以下略
```

Everyone(一番下の名前)をクリックすると以下のようなalertダイアログが出ます。

```json
{ name: 'Everyone',
  onClick: [Function: handleClick],
  setDeleteMember: [Function: handleDeleteList],
}
```

どうやら、呼び出された側のstate

```ts
interface UserListState {
    names: StringT[];
    deleteList: StringT[];
    pushed: StringT;
//    update: boolean;
}
```

ではなく、呼び出した側のstateがthisとして渡ってきました。

```ts
interface WelcomeState {
  name: StringT;
  changeName: StringN;
  checked: boolean;
}
```

this.stateがundefinedになるので、このエラーが出ていたようです。
thisの範囲がちがっているようですので、Welcomeを呼ぶときにdeleteListを追加しました。

```ts
interface WelcomeProps {
    name: StringT;
    onClick: (e:React.MouseEvent<HTMLButtonElement>, user_name:StringT) => void;
    setDeleteMember: (user_name:StringT, listin:boolean, deletelist: StringT[]) => void;
    deleteList: StringT[]
  }
```

呼出の際は、

```ts
this.props.setDeleteMember(this.state.name, checked, this.props.deleteList)
```

として、与えられたプロパティを戻すようにして対応しています。
非同期で更新されるはずなのに、propsで渡したときはその場で変更してもいいのかどうかなど、疑問は残ります。
一旦、この形で書いて動かします。

もっと違う書き方がないかなど、もう少しReactに慣れてきたら調べよう。

### 削除処理

deleteListは生成できたので、削除ボタンを押した際にちゃんとリストを参照できるかを調べてみます。

```ts
handleDelClick() {
  alert(this.state.deleteList)
}
```

チェックを入れてないときは、何も出ませんが1つでもクリックすると名前が入ります。
deleteListの更新もうまく行っているみたいなので、この先を作ります。

やりたいことは
- ボタンを押す
- 該当する名前を配列から削除
- 再表示
です。

ボタンが押されたら配列を更新すればいいんだろう、ということで以下のように書きました。

```ts
handleDelClick() {
  let localDL:StringT[] = this.state.deleteList
  let nameList:StringT[] = this.state.names

  for ( let i = 0; i < localDL.length; ++i) {
    if( nameList.includes(localDL[i])) {
      nameList = this.state.names.filter(n => n !== localDL[i])
    }
  }

  this.setState({
    names: nameList,
  })
}
```

namesにdeleteListの内容が含まれているかを順番に確認します。
存在しているときは、filterを使って名前を削除します。

やりたいことは、そういうことなのですが、実際に入れてみると最後の要素が取り除かれるのみで、フォームの順番は変わらず、削除されたはずの名前もそのままになりました。

これは期待した動作とは違います。

いろいろ調べていきましたが、React側がプログラム側でどの要素に更新があったかわからないのが原因ではないかと考えました。
配列とKeyの項目にも、以下のような記載があります。

> 配列内で使われる key はその兄弟要素の中で一意である必要があります。
> しかし全体でユニークである必要はありません。

そこで、Welcomeは配列で扱っているので、keyにユーザー名をいれてみることにしました。

```ts
return <Welcome key={user_name}
                name={user_name}
                onClick={this.handleClick}
                setDeleteMember={this.handleDeleteList}
                updateDeleteMember={this.updateDeleteMember}
                deleteList={this.state.deleteList}
        /> 
```

これで、削除した項目が正しく削除されるようになりました。
前回読んで試したときには、動作しているかもわかりませんでした。実際に使うと納得できます。

### チェックをいれてから名前が変更される

削除用にチェックを入れて、ボタンが押されるまでに名前が変更されたらどうしたらいいでしょうか。

本来であれば、チェックをいれた名前はもう削除する予定なので、その部分は変更不可の状況にしてしまうのが簡単です。
例えば、現状のonChangeは、その場でsetStateしていますから、入力された文字はそのまま反映されています。

```ts
<input type="text" value={this.state.name} onChange={(e) => this.setState({name: e.target.value})} />
```

onChangeに関数を設定して、その中でthis.state.checkedの値がtrueであれば変更内容を反映しない処理にすることで、チェックが入ったら変更不可、そうでなければ変更するという動作になります。

```ts
  onChangeTextHandler(e:React.ChangeEvent<HTMLInputElement>) {
    if (!this.state.checked) {
      this.setState({name: e.target.value})
    }
  }  

    ...

  render() {
        <input type="text" value={this.state.name} onChange={(e) => this.onChangeTextHandler(e)} />
```

そうすれば、何の問題もなかったんです。

しかし、これは別の方法を実装してから考えた内容なので、むしろなんでこういう風なことが考えつかなかったんだろうと思ってまとめました。

実装した内容は、チェックが入っているときに名前が更新されたら、リストを更新する関数を呼び出す、です。

```ts
onChangeTextHandler(e:React.ChangeEvent<HTMLInputElement>) {
  const oldName: StringT = this.state.name
  this.setState({name: e.target.value})
  if (this.state.checked) {
    this.props.updateDeleteMember(oldName, this.state.name, this.props.deleteList)
  }
}  

    ....省略

updateDeleteMember(old_name: StringT, new_name: StringT, deletelist: StringT[]) {
  const idx = deletelist.indexOf(old_name)
  if ( idx >= 0) {
      deletelist.splice(idx,1)
      deletelist.push(new_name)    
  }
}
```

呼ばれた関数では、deleteListから古い方の名前を探して削除してから、新しい名前を追加しています。
これで、チェックボックスにチェックが入っても削除したい名前を間違うことはなくなりました。

どっちがいいのかわかりませんが、チェックされたらフォームをグレーアウトして編集不可にするほうが操作としては正しいのかもしれません。

### Hello!ボタンでエラー

またしてもエラーがでました。
削除リストのチェックボックスで出たのと同じく、Hello!ボタンでも押されたときに挨拶の準備をする関数でundefinedのプロパティは操作できないというエラーです。

同じようにutil.inspectで確認してみたところ、thisがWelcomeStateの内容でした。

同じようなことが2度も起きているので、さすがにこれは何かおかしいなと思い、ドキュメントを読み直しました。
そして、Welcomeを次のように書き換えました。

```ts
return <Welcome
        key={user_name}
        name={user_name}
        onClick={(e,t:StringT)=>this.handleClick(e,t)}
        setDeleteMember={(t:StringT,c:boolean) => this.handleDeleteList(t,c)}
        /> 
```

Welcomeのイベントハンドラに詳細を書いたところ、ちゃんとUserList側のstateへアクセスできるようになりました。
最初に出ていた TypeError: Cannot read property 'deleteList' of undefined の対策もこれでできました。

最初の書き方では、正しい形でリフトアップできていなくて、関数のみが呼び出されていたわけです。
ソースもすっきりしつつ、stateの管理を正しくsetStateで行えるようになりました。

最終的にチェックボックスにチェックがはいっているときは名前を編集できないようにして、挨拶が表示されている名前が削除されたら、挨拶も削除されるように修正して、今回は終了です。

[ここまでの内容を見られるようにしました](https://sato-c.github.io/hello-react-world/)