ReactでHelloWorldしてから、ちょっとずつ足していく #1 #2 #3
はじめに
今回は
- フォーム
- stateのリフトアップ
をやっていきます。
フォーム化
せっかくなので、フォームで名前の編集と人の削除、追加などができるようにしたいと思います。
追加については後回しで、まずは現在の配列の内容を編集できるフォームを作っていきます。
表示は、Welcomeがやっていますので、ここのボタンをテキスト入力とチェックボックスに変えます。
挨拶ボタンは残しておきたいので、名前から「Hello!」表示に変えました。
render() {
return (
<div>
<input type="checkbox" checked={false} key={this.state.name} />
<input type="text" value={this.state.name} key={this.state.name} />
<button onClick={this.state.onClick} >Hello!</button>
</div>
);
}
名前を変えたり、メンバーを削除する準備は整いました。
次は、削除や変更の情報を受け取るためにどうすればいいかです。
説明を一度読んだだけでは、ちょっとわからなかったのでここが多分一番の山場かと思います。
問題は枝葉の先から、幹に対してどうやって情報を戻していくかというところです。
handleChange(e: React.ChangeEvent, user_name: StringT) {
をUserListへ移動して、1つ上の要素で管理するように修正しました。
しかし、書き換えただけではエラーがでてしまいました。
TypeError: Cannot read property 'names' of undefined
handleChange [as onChange]
src/App.tsx:93
90 | handleChange(e: React.ChangeEvent<HTMLInputElement>, user_name: StringT) {
91 | alert("handleChange:" + e.target.value + "/" + user_name);
92 |
> 93 | const users:StringT[] = this.state.names;
| ^
94 | for ( let i = 0; i < users.length; ++i)
95 | {
96 | if ( users[i] === user_name ) {
View compiled
onChange
src/App.tsx:49
46 | return (
47 | <div>
48 | <input type="checkbox" checked={false} key={this.state.name} />
> 49 | <input type="text" value={this.state.name} key={this.state.name} onChange={e => this.state.onChange(e, this.state.name)} />
| ^
50 | <button onClick={this.state.onClick} >Hello!</button>
51 | </div>
52 | );
View compiled
▶ 22 stack frames were collapsed.
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error.
handleChangeの引数の問題だったようで、onChangeの部分を引数ありに修正しました。
これで、エラーはなくなりました。
render() {
return (
<fieldset>
<legend>このサイトに登録されているメンバーリスト</legend>
<button>チェックした人を削除</button>
{
this.state.names.map((user_name: StringT, index:number) => {
let key='key_' + index;
if ( user_name === undefined || user_name === '') {
key += index;
return <Welcome name='everyOne' onClick={() => this.handleClick('everyOne')} key={key} onChange={(e, user_name) => this.handleChange(e,user_name)}/>
} else {
key += user_name + index;
return <Welcome name={user_name} onClick={() => this.handleClick(user_name)} key={key} onChange={(e, user_name) => this.handleChange(e,user_name)} />
}
})
}
<div className="msg">{this.state.pushed ? <div>Hello! {this.state.pushed}</div>:''}</div>
</fieldset>
)
}
しかし、入力して更新されるところまではよかったのですが、1文字入力するごとにフォーカスを失うので入力しづらくて大変です。
これ、実際は1文字いれるごとにonChangeしちゃうんじゃなくて、ボタン押されたらチェンジでいいんじゃないかなと思いつつ調べていたところ、下記のサイトが見つかりました。
React.js loses input focus on typing
You should never use value as part of the key when users can edit the value.
ユーザーが編集可能な値をキーの一部として使わない という感じでしょうか。
実際、ここではkeyを生成するときにユーザー名をキーにしてますので、思いっきりこの項目に当てはまると言うことです。
このサイトの例では、結論として以下のように書かれています。
Also, remember that such problem can be caused by invalid keys higher in the hierarchy.
この問題は、ヒエラルキーの上位で間違ったキーが使われていたことということなので、今回のプログラムでも同じような間違いだったんだろうと思います。
この方法を解決するのはめんどうそうなので、input textを使う場合の作法のようなものを調べていきます。
あらためてフォーム内容の更新
9.フォームに以下の説明があります。
フォーム要素の value 属性が設定されているので、表示される値は常に this.state.value となり、
React の state が信頼できる情報源となります。handleChange はキーストロークごとに実行されて
React の state を更新するので、表示される値はユーザーがタイプするたびに更新されます。
onChangeを指定して、そこに入力のたびにstateを更新する関数を書く。
これでinputで編集された内容をstateに反映されるので、Reactは表示を更新するし、値の正当性も保証される。
ということのようです。
以下のように書き換えました。
<input type="text" value={this.state.name} onChange={(e) => this.setState({name: e.target.value})} />
チェックボックスについても同様ですが、要素の名前が違いました。
<input type="checkbox" checked={this.state.checked} onChange={(e) => this.setState({checked: !this.state.checked })} />
targetではなくて、checkedです。(もともとのタグでもこうなってます)
タグに名前をつけて、onChangeが呼ばれた場合どのような要素が入るのかを調べるために以下のように修正してみました。
onChangeHandler(e:React.ChangeEvent<HTMLInputElement>) {
alert(e.target.name)
this.setState({checked: !this.state.checked })
}
... 略
<input type="checkbox" name={this.state.name} checked={this.state.checked} onChange={(e:React.ChangeEvent<HTMLInputElement>) => this.onChangeHandler(e)} />
このチェックボックスがチェックされたときにnameではtextに入っている内容をnameプロパティに渡しています。
alertでこの内容を表示させたところ、textの内容と同じ文字列が入っていましたので、このあたりは問題なさそうです。
このあたりが、各要素におけるstateの保証ということになるのでしょうか。
また、説明では(e)とだけ書かれているイベントですが、Typescriptになる場合は当然のことながら型がないとエラーになります。
エラーを修正するために試行錯誤してみましたが、最終的にはReact.ChangeEventを指定すれば問題ないようです。
さらっと2行で書いていますが、イベントに関する情報を調べきれず、エラーメッセージを頼りに指定を変えたらコンパイルが通って実行できたということなので、ここについてはリファレンスを調べておかないとあとでよくわからないこと(これを取ると動かない的な)が起こるので、注意しておきます。
要素を追加する
削除機能は、もう少し調べることが多そうなので、先にメンバー追加ボタンをつけます。
ボタンが押されたら配列要素を追加すればいいか、と考えたのでUserListに新しい関数を追加して、renderにボタンを追加しました。
handleAddClick() {
let newmembers: StringT[] = this.state.names;
newmembers.push("New Member" + newmembers.length)
this.setState({names: newmembers})
}
handleDelClick() {
}
render() {
return (
<div>
<div className="msg">{this.state.pushed ? <div>Hello! {this.state.pushed}</div>:''}</div>
<fieldset>
<legend>このサイトに登録されているメンバーリスト</legend>
<button onClick={()=>this.handleDelClick()}>チェックした人を削除</button>
<button onClick={()=>this.handleAddClick()}>新メンバー追加</button>
{
this.state.names.map((user_name: StringT, index:number) => {
if ( user_name === undefined || user_name === '') {
return <Welcome name='everyOne' onClick={(e:React.MouseEvent<HTMLButtonElement>, user_name:StringT) => this.handleClick(e,'everyOne')} />
} else {
return <Welcome name={user_name} onClick={(e:React.MouseEvent<HTMLButtonElement>, user_name:StringT) => this.handleClick(e,user_name)} />
}
})
}
</fieldset>
</div>
)
}
今回から、新しいタグでフォームを書き直しています。
fieldsetはフォーム要素の記載
legendはその内部でタイトルの記載
ということなので、ちょっと画面構成が変わりました。
handleAddClickはstateに入っているメンバーリストを取りだして、そこに新規要素を追加します。その後stateを更新することで追加した内容で表示が更新されます。
handleDelClickは、まだ内容を作っていませんがチェックが付いているメンバーをstateのメンバーリストから削除する動作になります。
次回は、この削除する動きについて考えていこうと思います。
本文にソース全部をいれるのは見づらくなってきたので、途中経過もgithubに入れるようにしました。
hello-react-world