見出し画像

React.js チュートリアル[超入門] #05 Stateを使った情報管理

前回のレッスンでは、stylingの方法について学びました。今回は、React特有のコンセプトであるstateをつかった情報管理を学習します。

前回までの作業用フォルダーは、僕のgithubから確認/ダウンロードできます。

このレッスンの目標
・Stateのコンセプトを理解する
・Class component vs functional component
・Stateを初期化してみる
・Stateを取得してみる
・Stateを更新してみる
・Stateを使って表示する要素を変更してみる
・Stateとpropsの違いについて考える

このレッスンでは、Google Chrome Developer Toolsを使います。
使ったことのない方は、ドットインストールに丁度いい授業があるので、簡単に予習しておくとレッスンが捗ります。


1. Stateのコンセプトを理解する

stateとは?
React.jsでは、component上でやりとりされる情報をstate(状態)として管理していきます。例えば、ユーザーのログイン情報だったり、投稿失敗時のエラー文だったりです。

stateの役割
stateを使うと動的にユーザーに表示されているコンテンツを変えることができます。例えば、ユーザーがログインしているかどうかをstateに管理している場合。そのstateの値に応じて、ログインページを表示するか、トップページを表示するか変えることも簡単にできます。また、ログインした後、stateが更新されると変更が必要なcomponentを自動で再読みしてくれるので、スムーズなUXを実現できます。とても便利です。


2. class component vs functional component

# 03 componentでも簡単に触れましたが、componentは主に二種類に分けることができます。class componentとfunctional componentです。このチュートリアルでも既に二つを使い分けています。

class component
class componentは、stateを持つことのできるcomponentです。event handler を定義したり、他のcomponentにpropsとして情報を渡したりできる高機能なcomponentです。

class componentは典型的に以下のような構造をしています。

// ComponentもReactと一緒にインポートする
import React, { Component } from 'react';

//classの宣言
class コンポーネント名 extends Component {

  //Event handlerやstateを定義する場所

  //render()メソッド
  render() {

    //return
    return (
      
    );
  }
}
export default コンポーネント名; 

今回のプロジェクトでは、App.jsがclass componentにあたります。ではclass componentの構造を知った上でApp.jsに同じ要素があるか、確認しましょう。コメント文に注目してください。

// App.js

// ComponentもReactと一緒にインポートする → OK
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import Title from './components/Title/Title';

//class componentの宣言 → OK
class App extends Component {


  //Event handlerやstateを定義する場所 → OK
  onClickHandler = () => {
    let title = document.getElementById('versionCounter');
    let upgradeButton = document.getElementById('upgradeButton');
    title.textContent = "4.0";
    upgradeButton.style.display = "none";
  }

  //render()メソッド → OK
  render() {

    //return → OK
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <Title
            title="Hello World 3.0"
            titleStyle={{color: 'orange'}}
            onClick={this.onClickHandler}
          >
            Hello World <span id="versionCounter" style={{borderBottom: '1px solid orange'}}>3.0</span>
          </Title>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

どうでしょう?すべて確認できたと思います。次は、functional componentを説明していきます。

functional component
functional componentは、class componentと違い機能がほとんどありません。又、stateを持つことができません。functionalって名前まぎらわしいですよね(笑)。ではfunctional componentの典型的な構造を紹介します。

// Componentをインポートする必要はありません。
import React from 'react';

// functional componentの宣言
const コンポーネント名 = () => {

  //return
  return(
    
  )
}

export default コンポーネント名;

今回のプロジェクトでは、Title.jsがfunctional componentにあたります。App.jsと同様に構造をチェックしてきます。

// Title.js

// Componentをインポートする必要はありません。→ OK
import React from 'react';
import './Title.css';

// functional componentの宣言→ OK
const Title = (props) => {

 //return→ OK
 return(
   <div className="titleSection">
     <h2
       style={props.titleStyle}
       id="versionStatement"
     >{props.children}</h2>
     <p
       onClick={props.onClick}
       id="upgradeButton"
       className="upgrade-button"
     >Upgrade</p>
   </div>
 )
}

export default Title;

インポートのルール
class componentは、両種類のcomponentをインポートすることができます、一方、functional componentは、functional componentのみインポートすることができます。functional componentがclass componentの親要素になることはできないのです。

このプロジェクトでいうと、App.jsがTitle.jsをインポートできても、Title.jsがApp.jsをインポートすることはできないということです。

二種類のcomponentを使い分ける
「結局どっちを使えばいいの?」って思いますよね。今までの説明を聞いてると、functional componentは制限も多いし、機能がしょぼいので全部class componentにしたくなるはず。しかし、React.jsでは、可能な限りfunctional componentを使うことを推奨しています。その一番の理由としては、stateやその他メソッドの管理が難しくなるからです。いろんな情報がいろんなclass componentによって管理されるより、少数のclass componentがstateを管理していて、その情報が必要な時だけpropsでfunctional componentに渡す、というのがReact流です。


3. Stateを初期化する

では実際にstateを導入してみましょう。class componentであるApp.jsを編集していきます。

まず、stateを初期化することが最初のステップです。class componentを宣言した直後に、stateの初期値を設定していきましょう。「state = { キーの名前: バリューの値}」の形式でstateの初期化&初期値の設定を行うことができます。

// App.js

class App extends Component {

 state = { version: '1.0'}
 

 render() {
   return (
        // ・・・・省略
   );
 }
}

今回は、Hello Worldの数字部分をstateで管理したいため、「state = { version: '1.0'}」と設定しました。

注意:constructorというメソッドを使ってstateの初期化を行なっているチュートリアルを見かけることがあります(下のコードを参照)。どちらの書き方も正解です。最終的にbabelというnode.jsのパッケージがコードを変換して全く同じ形で出力されます。

// App.js

class App extends Component {
 constructor(){
   super()

   this.state = { version: '1.0'}
 }

 render() {
   return (
        // ・・・・省略
   );
 }
}

4. Stateを取得してみる

では、stateに初期値として設定した「 version: '1.0' 」を取得して、「Hello World 1.0」と表示させてみましょう。stateの情報には、「this.state.任意のキーの名前」でアクセスできます。

// App.js

<Title
  titleStyle={{color: 'orange'}}
  onClick={this.onClickHandler}>

  Hello World 

  <span id="versionCounter" style={{borderBottom: '1px solid orange'}}>

    //stateの情報を取得
    {this.state.version}
  </span>

</Title>


5. Stateを更新してみる

では、次にstateの更新を学びます。試しに、Upgradeボタンを押すと、「Hello World 1.0」のversionが1づつ上がっていくように実装していきましょう。今回ボタンは消えません。onClickHandlerを以下のように変更してください。

// App.js 

//変更前

  onClickHandler = () => {
    //今回必要のない部分は削除
    let title = document.getElementById('versionCounter');
    let upgradeButton = document.getElementById('upgradeButton');

    //Title内の文字はstateで定義しているので、ここも変更します。
    title.textContent = "4.0";

    //Buttonを消さないので、このコードも削除
    upgradeButton.style.display = "none";
  }

//変更後

  onClickHandler = () => {
    
    let nextVersion = parseInt(this.state.version, 10) + 1
 
    this.setState({ version: nextVersion.toFixed(1)})
  }

parseInt(文字列, 基数)は、文字列を数字に変換する関数です。足し算をするために使用しました。10進数をつかっているので、基数の引数を10に設定します。

toFixedメソッドは小数点の桁数を指定させてくれます。今回バージョンを小数点第一位まで表示させたかったので、1にしました。

stateは、this.setState({キーの名前: バリューの値 })で変更することができます。下のようになれば成功です。再生速度の関係で、長押しすると光速アップグレードされるみたいに見えますが、普通にクリックしてます。


Stateはimmutable(変更不可)

stateを変更したい時、this.state = {キーの名前: バリューの値}としてしまいがちですが、これはNGです。stateは、immutableという性質を持っています。 直接変更することができないという意味です。「いやさっき変更したじゃん」って思いますよね。実は、直接stateを変更したわけではなく、setStateを使うことによって新しい値を代入したのです。言葉のあやのようでわかり難いと思いますが、このルールは、React.jsを理解すればするほど重要さを増してきます。心の片隅に置いておいてください。 

僕の言っていることが本当に正しいかどうか、試しにthis.state =を使って、stateを直接変更してみましょう。

// App.js 

  onClickHandler = () => {
    let nextVersion = parseInt(this.state.version, 10) + 1

    // this.setState({ version: nextVersion.toFixed(1)})
    this.state = { version: nextVersion.toFixed(1)}
  }

Google Chrome Developer Tools で確認していきます。そうすると、下のようなエラーが出ているはずです。

「Do not mutate state directly. Use setState()」とは「直接stateを変更しないでください。setState()を使いましょう」という意味です。やはりNGでした。忘れずにevent handlerをもとのコードに戻しましょう。

// App.js

  onClickHandler = () => {
    let nextVersion = parseInt(this.state.version, 10) + 1
    this.setState({ version: nextVersion.toFixed(1)})
  }

しつこいですが、重要なのでもう一度(笑)。

stateは、immutableという性質を持っています。 変更することができないという意味です。this.state = の代わりに、this.setStateを使いましょう。


Stateを更新するとcomponentが再度読み込まれる

もう一つ覚えておきたいのは、stateを更新すると、変更が必要なcomponentが自動で再読み込みされるということです。ここでは、App componentのrender部分が、ボタンを押すたびに、高速で読み込まれています。

console.logを使って確認してみましょう。renderメソッドのすぐ下に以下のコードを挿入してください。

// App.js

  onClickHandler = () => {
    let nextVersion = parseInt(this.state.version, 10) + 1
    this.setState({ version: nextVersion.toFixed(1)})
  }

  render() {

    if(parseInt(this.state.version, 10) > 1){
      console.log('アップグレード成功じゃい!! 新しいバージョンは ' + this.state.version);
    }

    return (
    //...省略

1回目の読み込み時に、console.logされて欲しくなかったので、if(parseInt(this.state.version, 10) > 1)を付け加えました。Google Chrome Developer Toolsを開いた状態で、これを実行して、Upgradeボタンを何回か押してみてください。下のようにコンソールに表示されたはずです。


この辺で初期化から変更後の流れを図で確認してみましょう。

何がどの順番で起きるのかを把握するのは重要です。後ほどでてくるライフサイクルメソッドを学ぶとさらに複雑になってきます。


6. Stateを使って表示する要素を変えてみる

先ほどはstateを使って、文字列を動的に変更しました。今度は、表示する要素そのものを変えてみます。10回以上ボタンを押すと、ボタンが「Already up-to-date」となって、クリックできないようにしましょう。if文とCSSを使って実装していきます。

まずボタンをApp.jsで管理やすい状態にするため、Title.jsのpタグをApp.jsにあるTitlle componentの直後に移します。

// Title.js

    <div className="titleSection">
      <h2
        style={props.titleStyle}
        id="versionStatement"
      >{props.children}</h2>

        
      // ここからコピペ
      <p
        onClick={props.onClick}
        id="upgradeButton"
        className="upgrade-button"
      >Upgrade</p>
        
      // コピペ終了。 この部分は削除してください。

    </div>

componentを別の場所に移した時にチェックすべきなのが、
・propsで渡した情報
・event handlerの呼び出し
・CSSの場所
・インポート・エクスポートした要素の相対パス

これらの微調整を行なっていきます。コード内のコメントを参照して、何をどう変更しているか確認してください。

// App.js (一部抜粋)


// onClick=this.onClickHandlerを削除してください。
<Title
  titleStyle={{color: 'orange'}}
>
  Hello World
    <span id="versionCounter" style={{borderBottom: '1px solid orange'}}>
      {this.state.version}
    </span>
</Title>

// ここに先ほどのpタグを貼り付けました。 
// props.onClick を this.onClickHandler に書き換えましょう。
<p
  onClick={this.onClickHandler}
  id="upgradeButton"
  className="upgrade-button"
>Upgrade</p>
// Title.js

// App.jsもTitle.jsも同じタイミングで
// 読み込まれるcomponentなのでここでの作業を行わなくても、
// エラーは出ません。
// componentに対応したCSSファイルに書いていくのがわかりやすいです。


// 全部App.cssにコピペで移してください。 
.upgrade-button{
 padding: 5px 20px;
 border-radius: 5px;
 display: inline-block;
 border: 1px solid #fff ;
 cursor: pointer;
}

.upgrade-button:hover{
 opacity: .8;
}

準備ができたので、早速stateを活用していきましょう。App.jsを以下のように変更します。

①まずupgradeButtonという要素を代入するための変数を定義します。これまで使ってきたボタンを初期値として代入してください。

②versionが「5.0」になった時点で、変数upgradeButtonに別のボタン要素を代入します。if文を利用しましょう。このボタンには、upgraded-buttonというクラス名をつけました。

③定義したupgradeButtonをJSXに挿入します。Title componentの直後にある先ほどコピペしたpタグボタンを{upgradeButton}で置き換えてください。JSXの中では{ }で変数を参照することができます。

// App.js (一部抜粋)

render(){

    // ①upgradeButton初期の要素
    let upgradeButton = (
      <p
        onClick={this.onClickHandler}
        id="upgradeButton"
        className="upgrade-button"
      >Upgrade</p>
    );

     // ②upgradeButton初期の要素
    if( this.state.version === '5.0'){
      upgradeButton = (
        
        //バージョンが5.0になった時に表示されて欲しいボタン
        <p
          className="upgraded-button"
        >Already up-to-date</p>
      );
    }

    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <Title
            titleStyle={{color: 'orange'}}
          >
            Hello World
              <span id="versionCounter" style={{borderBottom: '1px solid orange'}}>
                {this.state.version}
              </span>
          </Title>

          // ③ここでupgradeButtonを呼び出す。 
          // 先ほどコピペしたpタグは削除しましょう。
          {upgradeButton}
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

せっかくなのでApp.cssでupgraded-buttonクラスにもstylingをしていきます。

// App.css (一部抜粋)
// upgrade-buttonの直後ぐらいがわかりやすいと思います。

.upgraded-button{
  padding: 5px 20px;
  border-radius: 5px;
  display: inline-block;
  border: 1px solid #aaa ;
  color: #aaa ;
  pointer-events: none;
}

下のように、うまく入れ替われば成功です。

今回使ったletで変数を定義して、if文で代入する要素を入れ替える手法は、いろんな場面で役に立ちます。例えば、ユーザーのリストをサーバーからとってきて表示する場合。最初componentを読み込んだ段階では、ユーザーの情報はまだ取得できていないので、let userList = <Spinner />; と指定しておいて、ユーザー情報が取得できるまでの間、ローディング画面を表示することもできます。(Spinner componentという円がぐるぐる回るcomponentがあることを前提とした話です。)

7. Stateとpropsの違いについて考える

stateとpropsはよく混同して認識されるコンセプトです。今回のチュートリアルでは、あえて別々に紹介してきました。どちらもcomponentの情報を保持しているので、区別が難しいのだと思います。googleで、「react.js stateとは」と検索するだけで「stateとpropsの違い」のような記事がたくさん表示されます。聞いてもいないのに(笑)。 

情報を初期化したり更新したりするものをstate、情報を左から右に渡していくのがpropsだと僕は認識しています。実際の開発では親componentのstateで管理している情報を子componentにpropsとして渡していく、という流れが非常に多いです。今わからなくても場数を踏んで違いをはっきりさせていきましょう。

もう少し深く掘り下げていきたい人は、下のような記事を読むといいです。


まとめ


今回のレッスンは以上です。ボリューミーでしたね(汗)。

#05 Stateを使った情報管理
✔︎ Stateのコンセプトを理解する
✔︎ Class component vs functional component
✔︎ Stateを初期化してみる
✔︎ Stateを取得してみる
✔︎ Stateを更新してみる
✔︎ Stateを使って表示する要素を変更してみる
✔︎ Stateとpropsの違いについて考える

Stateは、React.jsがReact.jsらしくあるための超重要なシステムです。しっかり、理解しましょう。英語ができる方には、公式ドキュメントを読むことをオススメしています。

残念ながら今回のレッスンで、Hello Worldをいじるのはお終いです。次のレッスンでは、今まで学んだ重要なコンセプトの総復習をします。それが終わるといよいよ、NekoTubeに移っていきます。さよなら「Hello World」こんにちは「NekoTube」。自分で言っていいのかわかりませんが、めっちゃ楽しいです。たくさん手を動かすことになるので、心の準備をよろしくお願いします!

質問があればいつでも、コメント欄やTwitterで受け付けてます。

次のレッスン

近日公開予定

この記事が気に入ったらサポートをしてみませんか?