11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Reactの基本メモ

Last updated at Posted at 2019-10-23

概要

Reactの公式ドキュメントを読みながら勉強した際の備忘メモです。
Reactを動かしながら学習したかったので、環境はDockerでサクッと作った感じです。
(環境構築は記述していないです。)

前提

Javascriptの知識
自信のない方は、Javascriptチュートリアルで知識を確認してから進める。

React Documents

公式DocsのMAIN CONCEPTを進めていきました。
項目1.Hello Worldは飛ばして、次項の2.JSXの導入からになります。
またドキュメント内のJavascriptのおさらい的な部分は省いています。
最後の12.Reactの流儀は検索可能な商品データ表をReactで作っていく様子が記述されています。
流儀と言われ畏れ多いので、公式ドキュメントに任せます。(面倒なだけ)

1.Hello World

前提の話などなので、飛ばします。

2.JSXの導入

javascriptの拡張構文(プログラミング言語)
導入は必須ではないが、有用な見た目、有用なエラー警告を多く表示してくれる。

式の埋め込み

{}(中括弧)でjavscriptの式を利用できる

embed.js
const name = 'taro';
const element = <h1>Hello {name}</h1>;

ReactDOM.render(element, document.getElementbyId('root'));

JSXも式である

コンパイル後、JSXは普通のJavascriptの関数呼び出しに変換されて、Javascriptオブジェクトとして評価される。

formula.js
function getGreeding(user){
  if(user){
    return <h1>Hello {user}.</h1>;
  }
  return <h1>Hello foo.</h1>;
}

属性指定

文字列リテラルを属性として指定するために""(引用符)を使用できる。
また、Javascript埋め込む際は、{}(中括弧)を使用する。
Javascriptを使用時には、中括弧を囲む引用符を使用しない。

str.js
const element = <div className="string"></div>;  // 文字列

const foo = "string";
const fooElement = <div className={foo}></div>;  // Javascript

const fighters = "dave";
const fightersElememt = <div className="{fighters}"></div>; //  NG

キャメルケースのプロパティ命名規則

JSXはHTMLよりもJavascriptに近いので、HTML属性ではなく、キャメルケースの命名規則を使用する。

class.js
<div class="class_name"></div> // NG
<div className="class_name"></div> // OK

子要素指定

子要素を持つことができる。
また、中身がない場合はタグを閉じる(/>する)。

child.js
const child = (
  <div>
    <h1>Hello React</h1>
    <h2>Hello JSX</h2>
  </div>
);

const close = <img src={foo} />;

JSXはインジェクション攻撃を防ぐ

デフォルトでReactDOMは、JSXに埋め込まれた値をレンダリングされる前にエスケープするため、レンダーの前に全てが文字列に変換される。これはXSS攻撃の防止になる。

xss.js
const title = response.malInput;
const element = <h1>{title}</h1>;

JSXはオブジェクトの表現

BabelはJSXをReact.createElement()の呼び出しへコンパイルする。
下記の2つは同じになる。

element.js
const element = (
  <h1 className="greeting">
    Hello React.
  </h1>
);


const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello React.'
);

3.要素のレンダー

要素をDOMとして描画する

Reactだけで構成されたアプリケーションは、通常一つのDOMノードだけを持つ。
React要素をDOMノードにレンダリングするには、
ReactDOM.render()に、React要素(第1引数)とDOMノード(第2引数)を渡す。

root.html
<div id="root"></div>

rootのidを持つDOM要素に、elementをレンダーする

root.js
const element = <h1>Hello React</h1>;
ReactDOM.render(element, document.getElementbyId('root'));

レンダリングされた要素の更新

React要素はImmutableのため一度作成すると変更ができない。
更新する方法は、新しい要素を作成して、ReactDOM.render()に渡す。
以下は、秒ごとに動く時計の例です。

clock.js
function Clock(){
  const element = (
    <div>
  	  <h1>It is {new Date().toLocaleTimeString()}.</h1>
	</div>
  );
  ReactDOM.render(element, document.getElementbyId('root')
  }

setInterval(clock, 1000);

必要な箇所のみ更新する

ReactDOMは要素とその子要素を、以前のものと比較し必要なだけの要素を更新する。

4.componentとprops

コンポーネントを定義する方法としては、javascriptの関数、またはes6のクラスを記述する。
propsという任意の入力を受け取りReact要素を返す。
詳細はコンポーネントAPIリファレンス

関数 component

データの入ったpropsというオブジェクトを引数として受け取り、React要素を返す。

func.js
function Hello(props){
  return <p>Hello {props.name}.</p>;
}

クラス component

class.js
class Hello extends React.Component{
  render(){
    return <p>Hello {this.props.name}.</p>;
  }
}

componentのrender

DOMのタグを表すReact要素だけではなく、ユーザ定義のcomponentを表すことができる。
Reactがユーザ定義のコンポーネントを見つけた場合、JSXの属性を単一のオブジェクトとして、コンポーネントに渡す。
このオブジェクトのことはpropsと呼ぶ。

comp.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

const element = <Hello name="aiueo" />;

ReactDOM.render(
  element,
  document.getElementById('root')
);

上記の処理の流れ
1: <Hello name="aiueo" />という要素を引数として、ReactDOM.render()を呼び出す。
2: Helloコンポーネントを呼び出し、propsとして、{name: 'aiueo'}を渡す。
3: Helloコンポーネントは<p>Hello aiueo</p>を返す。
4: ReactDOMは<p>Hello aiueo</p>に一致するように効率的に更新する。

コンポーネントを組み合わせる

コンポーネントは自信の出力の中で、他のコンポーネントを参照できる。
↓Helloを何回もレンダリングするAppを作成する。

conbine.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

function App(){
  return(
    <div>
	  <Hello name="Taro" />
	  <Hello name="Jiro" />
	  <Hello name="Saburo" />
	</div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

componentを抽出する

コンポーネントを分割する(コンポーネントをより小さいコンポーネントに分割することを恐れないでください、とのこと)。
以下のCommentコンポーネントについて考えてみる。

base.js
function Comment(props){
  return(
    <div className="comment">
	  <div className="userinfo">
	    <img 
		  src={props.author.imageUrl}
		  alt={props.author.name}
		  className="avatar"
		/>
		<div className="username">
		  {props.author.name}
		</div>
	  </div>
	  <div className="text">
	    {props.text}
	  </div>
	  <div className="date">
	    {formatDate(props.date)}
	  </div>
	</div>
  );
}

多くのネストがあるため、内部の個々の部品の再利用は困難になる。

↓ ここでコンポーネントを抽出する

userinfo部分を抽出する
自身がComment内にレンダリングされていることは知っておく必要はない。
そのため、コンポーネントのコンテキストからではなく、自身のコンポーネントの観点からpropsの名前を定義する。

avatar.js
function Avatar(props){
  return(
	<img 
	  src={props.user.imageUrl}
	  alt={props.user.name}
	  className="avatar"
	/>
  );
}

userinfo.js
function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
	    {props.user.name}
      </div>
    </div>
  );
}

↓ よりシンプルにする。

alternateBase.js
function Avatar(props){
  return(
	<img 
	  src={props.user.imageUrl}
	  alt={props.user.name}
	  className="avatar"
	/>
  );
}

function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
	    {props.user.name}
      </div>
    </div>
  );
}

function Comment(props){
  return(
    <div className="comment">
      <UserInfo user={props.author} />
      <div className="text">
	    {props.text}
	  </div>
	  <div className="date">
	    {formatDate(props.date)}
	  </div>
	</div>
  );
}

propsは読み取り専用

Reactは柔軟だが、1つ厳格なルールがある。
コンポーネントは自分自身のpropsは決して変更してはいけない。
以下、公式ドキュメントから引用

全ての React コンポーネントは、自己の props に対して純関数のように振る舞わねばなりません。

5.stateとライフサイクル

componentのカプセル化

Clockコンポーネントを再利用可能かつカプセル化する。

clock.js
function Clock(props){
  return(
    <div>
      <p>Hello React</p>
	  <p>{props.date.toLocaleTimeString()}</p>
	</div>
  );
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
	document.getElementById('root')
  );
}

setInterVal(tick, 1000);

↑ UIを毎秒ごとに更新する処理は、Clockの内部実装であるべき処理なので、ReactDOM.render()部分を一度のみ記述して、Clock自身を更新させたい。
ここでstateを利用する。
statepropsに似ているが、コンポーネントに管理されるプライベートなもの。

関数をクラスに変換する

以下の流れで関数コンポーネントをクラスに変換する。
1.React.Componentを継承する同名のes6のクラスを作成。
2.render()と呼ばれる空のメソッドを1つ追加。
3.関数の中身をrender()メソッドに移動。
4.render()内のpropsthis.propsに書き換える。
5.空になった関数の宣言部分を削除。

以下コード

clock.js
class Clock extends React.Component{
  render(){
    return (
	  <div>
         <p>Hello React</p>
		 <p>{this.props.date.toLocaleTimeString()}</p>
	  </div>
	);
  }
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
	document.getElementById('root')
  );
}

setInterVal(tick, 1000);

これにより、render()は更新した際に毎回呼ばれるが、同一のDOMノード内で<Clock />をレンダーしている限り、Clockクラスのインスタンスは1つだけ使われる。
このことにより、ローカルstateやライフサイクルメソッドといった追加機能を利用できる。

ローカルstateを設定

以下のステップでdatepropsからstateに移動する。
1.render()メソッド内のthis.props.datethis.state.dateに書き換える。
2.this.stateを初期化するクラスコンストラクタを作成する。
3.<Clock />要素から、dateプロパティを削除する。

以下コード

clock.js
class Clock extends React.Component{
  constructor(props){
    // クラスのコンポーネントは常に props を引数として親クラスのコンストラクタを呼び出す必要がある。
    super(props);  
    this.state = {
      date: new Date()
    };
  }
  
  render(){
    return (
	  <div>
         <p>Hello React</p>
		 <p>{this.state.date.toLocaleTimeString()}</p>
	  </div>
	);
  }
}

function tick(){
  ReactDOM.render(
    <Clock />,
	document.getElementById('root')
  );
}

setInterVal(tick, 1000);

ライフサイクルメソッド

多くのコンポーネントを持つアプリケーションでは、コンポーネントが破棄された場合にそのコンポーネントが持っていたリソースを開放することがとても大切。
Clockを例にすると、

・タイマーを設定したい時 => DOMとして描画される時(マウント(mounting)と呼ぶ)
・タイマーをクリアしたい時 => 生成したDOMが削除される時(アンマウント(unmounting)と呼ぶ)

コンポーネントクラスは特別なメソッドでマウント、アンマウントする時のコードを実行できる。
これらのメソッドはライフサイクルメソッドと呼ばれる。詳しくはReact.Component – Reactで確認。

clock.js
class Clock extends React.Component{
  constructor(props){
    super(props);
	this.state = {
	  date: new Date()
    };
  }
  
  // 出力がDOMにレンダーされた後に実行される
  componentDidMount(){
    this.timerID = setInterval(
	  () => this.tick(),
	  1000
	);
  }
  
  // コンポーネントがDOMから削除されるときに呼び出される
  componentWillUnmount(){
    clearInterval(this.timerID);
  }
  
  // コンポーネントのローカルstateの更新をスケジュールするためにthis.setState()を使用する
  tick(){
    this.setState({
	  date: new Date()
    });
  }

  render(){
    return (
      <div>
        <p>Hello React</p>
        <p>{this.state.date.toLocaleTimeString()}</p>
      </div>
	);
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

上記の処理の流れ
1.<Clock />ReactDOM.render()に渡され、Clockコンポーネントのコンストラクタを呼び出す。
Clockは現在時刻を表示するので、現在時刻を含んだオブジェクトでthis.stateを初期化する。そしてこのstateを更新していく。

2.Clockコンポーネントのrender()メソッドを呼び出し、Reactは画面に何を表示すべきか知る。そして、DOMをClockのレンダー出力と一致するように更新する。

3.Clockの出力がDOMに挿入されると、componentDidMount()メソッドを呼び出し、Clockコンポーネントは毎秒ごとに tick()メソッドを呼び出すためにタイマーを設定するようブラウザに要求する。

4.ブラウザは、毎秒ごとにtick()メソッドを呼び出す。その中で Clockコンポーネントは、現在時刻を含んだオブジェクトを引数として setState()を呼び出し、UI の更新をスケジュールします。setState()が呼び出され、Reactはstateが変わったということが分かり、render()メソッドを再度呼び、画面上に何を表示すべきかを知る。
(今回は、render()メソッド内のthis.state.dateが異なるため、レンダリングされる出力は新しく更新された時間が含まれる。それに従って ReactはDOMを更新する。)

5.この後に ClockコンポーネントがDOMから削除されると、componentWillUnmount()メソッドを呼び、タイマーが停止する。

stateを正しく使用する

setState()について知っておくべきことが3つある。

1.stateについて、直接変更しない。
代わりにsetState()を使用する。constructorの中は唯一this.stateに直接代入していい。

one.js
const this.state.foo = 'abc'; // NG

2.stateの更新は非同期で行われる時がある。
this.propsthis.stateは非同期で更新されるため、それらの値に依存すべきではない。

two.js
// NG 更新に失敗することがある
this.setState({
  counter: this.state.counter + this.props.increment
});

// OK アロー関数ではなく、通常の関数でもOK
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

3.stateの更新はマージされる
setState()が呼ばれた際に、現在のstateの値にマージする。
stateが独立した変数を含んでいる場合、別々にsetState()を呼び出し、更新することができる。

three.js
constructor(props){
  super(props);
  this.state = (
    posts: [],
	comments: []
  );
}

componentDidMount(){
  fetchPosts().then(response => {
    this.setState(
	  posts: response.posts
	);
  });
  
  fetchComments().then(response => {
    this.setState(
	  comments: response.comments
	);
  });
}

データは下方向に伝わる

stateを所有してセットするコンポーネント自身以外からはそのstateにアクセスすることはできない。
コンポーネントはその子コンポーネントにpropsとして自身のstateを渡しても構わない。
どんなstateも必ず特定のコンポーネントが所有し、stateから生ずる全てのデータは、ツリーでそれらの下にいるコンポーネントにのみ影響する。

6.イベント処理

Reactのイベント処理はDOM要素のイベント処理に似ているが文法的な違いがある。
例えば、Reactのイベントは小文字ではなく、キャメルケースで表す。
JSXではイベントハンドラとして文字列ではなく、関数を渡す。

index.html
<button onclick="activeLacers()"></button>
index.js
<button onClick={acticeLacers}></button>

また、Reactではfalseを返してもデフォルトで抑止しないため、明示的にpreventDefaultを呼び出す

index.html
<a href="#" onclick="alert('this link was clicked'); return false">click me</a>
index.js
function ActionLink(){
  function handleClick(e){
    e.preventDefault();
	alert('this link was clicked');
  }
  
  return(
    <a href="#" onClick={handleClick}>click me</a>
  );
}

ここでのeは合成イベント

Reactの場合、DOM生成後にaddEventListenerを呼び出して、リスナーを追加するべきではない。
代わりに要素が最初にレンダリングされる際に、リスナーを指定する。

コンポーネントをクラスを使用して定義した場合、一般的なパターンでは、クラスのメソッドになる。

以下コード
※JSXのコールバックにおけるthisには注意する。
Javascriptのクラスのメソッドはデフォルトでバインドされないため、バインドする。

toggle.js
class Toggle extends React.Component{
  constructor(props){
    super(props);
	  this.state = {
	    isToggleOn: true
    };
	  this.hundleClick = this.hundleClick.bind(this);
  }

  hundleClick(){
    this.setState(state => ({
	    isToggleOn: !state.isToggleOn
    }));
  }

  render(){
    return (
      <button onClick={this.hundleClick}>
        {this.state.isToggleOn ? 'ON' :'OFF'}
      </button>
    );
  }
}

イベントハンドラに引数を渡す

ループ処理内で、イベントハンドラの追加のパラメータを渡したい場合
両方同じ処理

loop.js
<button onClick={(e) => this.deleteRow(id, e)}>Delete</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete</button>

どちらもReactのイベントを表すeという引数はidの次の2番目の引数として渡されることになる
アロー関数では明示的にeを渡す必要があるが、bindの場合はid以降の追加の引数は自動的に転送される

7.条件付きレンダー

Reactの条件条件付きレンダーはJavascriptにおける条件分岐と同じように動作する。

要素変数

要素を保持するために変数を利用できる。
コンポーネントの一部を条件付きでレンダーしたい場合に役立つ。

以下ログイン/ログアウトボタンを表すコード

logInOut.js
function UserGreeting(props){
  return (
    <p>Hello React!</p>
  )
}

function GuestGreeting(props){
  return (
    <p>Please Login!</p>
  );
}

function Greeting(props){
  const isLogin = props.isLogin;

  if(isLogin){
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

function LoginButton(props){
  return (
    <button onClick={props.onClick}>Login</button>
  );
}

function LogoutButton(props){
  return (
    <button onClick={props.onClick}>Logout</button>
  );
}

class LoginControl extends React.Component{
  constructor(props){
    super(props);
    this.hundleLoginClick = this.hundleLoginClick.bind(this);
    this.hundleLogoutClick = this.hundleLogoutClick.bind(this);
    this.state = {
      isLogin: false
    };
  }

  hundleLoginClick(){
    this.setState({
      isLogin: true
    });
  }


  hundleLogoutClick(){
    this.setState({
      isLogin: false
    });
  }

  render(){
    const isLogin = this.state.isLogin;
    let btn;

    if(isLogin){
      btn = <LogoutButton onClick={this.state.hundleLogoutClick} />
    }else{
      btn = <LoginButton onClick={this.state.hundleLoginClick} />
    }

    return (
      <Greeting isLogin={isLogin} />
    );
  }
}

ReactDOM.render(
  <LoginControl />,
  document.getElementById('root')
);

インラインif

以下、unreadMsgtrueなら&&以降が返され、falseならfalseが返される

inlineIf.js
function Mailbox(props){
  const unreadMsg = props.unreadMsg;
  return (
    <div>
      <p>Hello React!</p>
      {unreadMsg > 0 &&
        <p>You have {unreadMsg.length} unread message.</p>
      }
    </div>
  );
}

const msgs = ["foo", "red", "nir"];

ReactDOM.render(
  <Mailbox unreadMsg={msgs}/>,
  document.getElementById('root')
);

インラインif-else

可読性が悪い場合は使用しない

inlineElse.js
render(){
  const isLogin = this.state.isLogin;
  return (
    <div>
      The user is <b>{isLogin ? 'currently' : 'not'}</b> log in
	</div>
  );
}

コンポーネントのレンダーを防ぐ

render()の代わりにnullを返す。
コンポーネントのrenderメソッドからnullを返してもコンポーネントのライフサイクルメソッドのトリガーには影響しない。

preventRender.js
function WarningBanner(props){
  if(!props.warn){
    return null;
  }

  return(
    <div className="alert-danger">
      Warning!
    </div>
  );
}

class Page extends React.Component{
  constructor(props){
    super(props);
    this.hundleToggleClick = this.hundleToggleClick.bind(this);
    this.state = {
      showWarning: true
    };
  }

  hundleToggleClick(){
    this.setState(state => ({
      showWarning: !state.showWarning
    }));
  }

  render(){
    return(
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.hundleToggleClick}>
          {this.state.showWarning ? 'hide' : 'show'}
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById('train')
);

8.リストとkey

複数のコンポーネントをレンダリングする

要素の集合を作成し{}で囲むことでJSXに含めることができる。

multi.js
const memberNames = ["dave", "nick", "chester", "tom"];
const listItems = memberNames.map((member, index) => {
  return <li key={index}>{member}</li>
});

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('train')
);

基本的なリストコンポーネント

通常リストは、何かしらのコンポーネントの内部でレンダリングしたい。
上記のコードを例に、要素を出力するコンポーネントを作ることができる。

listComp.js
function MemberList(props){
  const memberNames = props.members;
  const listItems = memberNames.map((member, index) => {
    return <li key={index}>{member}</li> // keyに関しては次の節で
  });
  return(
    <ul>{listItems}</ul>
  );
}

const memberNames = ["dave", "nick", "chester", "tom"];
ReactDOM.render(
  <MemberList members={memberNames} />,
  document.getElementById('train')
);

key

どの要素が追加、変更、削除されたのかReactが識別するのに役立つ。
配列内の項目に識別の安定性を持たせるために、各項目にkeyを与えるべきである。

key1.js
const numbers = [1,2,3,4,5];
const listNumber = numbers.map(number => {
  return <li key={number.toString()}>{number}</li>
});

兄弟間で項目を一意に特定できるkeyを設定するのが最良、
多くの場合はデータ内のidkeyとして使用することになる。

key2.js
const todos = [
  {id: 1, text: "a"},
  {id: 2, text: "b"},
  {id: 3, text: "c"},
  {id: 4, text: "d"},
  {id: 5, text: "e"}
];
const todoItem = todos.map(todo => {
  return <li key={todo.id}>{todo.text}</li>
});

安定したidがない場合、項目のindexを使用することができる。

key3.js
const todos = [
  {text: "a"},
  {text: "b"},
  {text: "c"},
  {text: "d"},
  {text: "e"}
];
const todoItem = todos.map((todo, index) => {
  return <li key={index}>{todo.text}</li>
});

要素の並び順を変更される可能性がある場合は、indexkeyとして使用することは、パフォーマンスに悪影響を与え、コンポーネントの状態に問題を起こさせる可能性がある。詳しくは、Robin Pokornyの解説を参照。

keyのあるコンポーネントの抽出

keyが意味を持つのは、それを取り囲んでいる配列側の文脈になる。

例:ダメパターン

ngKey.js
function ListItem(props){
  const value = props.value;
  return(
    <li key={value.toString()}>{value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return <ListItem value={number} />
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

<ListItem />を抽出する際には、key<li>要素ではなく、配列内の<ListItem />要素に残しておくべき。

例:okパターン
基本ルールとしては、map()の呼び出し内の要素にkeyが必要。

okKey.js
function ListItem(props){
  return(
    <li>{props.value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return (
    <ListItem
      key={number.toString()}
      value={number}
    />
    );
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

keyは兄弟間の要素で一意であれば良い

二つの異なる配列を作成する場合では、同一のkeyを使っても構わない。

uniqueKey.js
function Blog(props){
  const sidebar = (
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );

  const content = props.posts.map(post =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );

  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: "hello", content: "react"},
  {id: 2, title: "bye", content: "javascript"}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('train')
);

keyはReactへのヒントとしては使われるが、記述したコンポーネントには渡されないため、別名のpropsとして明示的に渡す必要がある。

propsKey.js
const content = posts.map(post => 
  <Post
    key={post.id} // cannot read
    id={post.id} // can read
    title={post.title}
  />
);

Postコンポーネントはidは読み取れるが、keyは読み取れない。

map()をJSXに埋め込む

上記を例にlistItemsを別途宣言して、JSXに含める。

emb.js
function NumberList(props){
  const numbers = props.numbers:
  const listItems = numbers.map(number => 
    <ListItem 
      key={number.toString()}
      value={number}
    />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

map()の結果をインライン化することもできる。

embInline.js
function NumberList(props){
  const numbers = props.numbers:
  return (
    <ul>
      {numbers.map(number => 
        <ListItem 
          key={number.toString()}
          value={number}
        />
      )}
    </ul>
  );
}

可読性のためにコンポーネントを抽出する必要があるのかどうか考える必要はある。

9.フォーム

HTMLのフォームの送信に応答してユーザがフォームに入力したデータにアクセするようなjavascript関数があると便利

controlled component

Reactでは、変更される可能性があるものはstateプロパティに保持され、setState関数でのみ更新される。
そのためこの2つの状態を結合させることで、フォームをレンダーしているReactコンポーネントが後続のユーザ入力でフォームに起きることを制御できる。

例:フォーム送信時に名前をログとして残す。

logName.js
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = (
      {value: ''}
    );

  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert(`A name was submitted:${this.state.value}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <NameForm />,
  document.getElementById('train')
);

textareaはvalue属性を使用する。

textarea.js
class AreaForm extends React.Component{
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      value: 'Please write everything about your favorite thing.'
    };
  }

  handleChange(event) {
    this.setState(
      {value: event.target.value}
    );
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
          <textarea
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <AreaForm />,
  document.getElementById('train')
);

selectはselect属性があるので、親のselectタグのvalueを扱う。

select.js
class SelectForm extends React.Component{
  constructor(props){
    super(props);
    this.hundleChange = this.hundleChange.bind(this);
    this.hundleSubmit = this.hundleSubmit.bind(this);
    this.state = (
      {value: ''}
    );
  }

  hundleChange(event){
    this.setState({value: event.target.value});
  }

  hundleSubmit(event){
    alert(`Your favorite type is ${this.state.value}`);
    event.preventDefault();
  }

  render(){
    return(
      <form onSubmit={this.hundleSubmit}>
        <label>
          Pick your favorite type.
          <select value={this.state.value} onChange={this.hundleChange}>
            <option value="red">red</option>
            <option value="blue">blue</option>
            <option value="green">green</option>
            <option value="yellow">yellow</option>
            <option value="white">white</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}


ReactDOM.render(
  <SelectForm />,
  document.getElementById('train')
);

value属性に配列を渡すことで複数オプションを選択することができる。

selectMulti.js
<select multiple={true} value={['B', 'C']}>

複数入力の処理

複数のinput要素を制御するには、name属性を追加すれば、event.target.nameに基づいて、処理を選択することができる。

multiInput.js
class Reservation extends React.Component{
  constructor(props){
    super(props);
    this.hundleInputChange = this.hundleInputChange.bind(this);
    this.state = {
      isGoing: true,
      numberOfGuest: 2
    };
  }

  hundleInputChange(event){
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState(
      {[name]: value}
    );
  }

  render(){
    return(
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.hundleInputChange}
          />
        </label>
        <br />
        <label>
          Number of guest:
          <input
            name="numberOfGuest"
            type="number"
            value={this.state.numberOfGuest}
            onChange={this.hundleInputChange}
          />
        </label>
      </form>
    );
  }
}

ReactDOM.render(
  <Reservation />,
  document.getElementById('train')
);

制御された入力におけるnull値

制御されたコンポーネントでvalueプロパティに値を指定することでユーザーが値を変更できないようになる。

null.js
ReactDOM.render(<input value="hi" />, 
  document.getElementById('train'));

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

制御されたコンポーネントの代替手段

制御されたコンポーネントは、あらゆる種類のデータの変更に対してイベントハンドラをかく場合などは、入力フォームを実装する代替手段である非制御コンポーネント – Reactを検討する。

本格的なソリューション

完全なソリューションを探している場合はformikが人気がある選択肢の一つ。

10.stateのリフトアップ

いくつかのコンポーネントが同一の変化するデータを反映する必要がある。そのような時は最も近い共通の祖先コンポーネントにstateをリフトアップすることが良い。

コード例:水が沸騰するかどうか計算する

base.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    return(
      <fieldset>
        <legend>Enter temperature in Celsius.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

上記に二つ目の要素追加(華氏温度)。

addprops.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  render(){
    return(
      <div>
        <TemperatureInput scale='c' />
        <TemperatureInput scale='f' />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

片方のフィールドを変化させても、もう一方のフィールドの更新はされないため、二つのフィールドを同期させるようにする。

変換関数作成

trans.js
// 摂氏温度に変換
function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

// 華氏温度に変換
function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}


// 常に小数第3位までで四捨五入されるようにする
function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return null;
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

stateのリフトアップ

まだ両方のTemperatureInputコンポーネントは独立してローカル stateを保持しているが、同期していてほしい。
よって、stateをリフトアップして実現する。(TemperatureInputからCalculatorに移動する。)
以下の手順で行う。

1.TemperatureInputコンポーネントのthis.state.temperaturethis.props.temperatureに変える。
this.props.temperatureは既にあるものとする。(後でこれはCalculatorから渡すようにする)

first.js
class TemperatureInput extends React.Component{
// 省略
  render(){
    // const temperature = this.state.temperature;
    const temperature = this.props.temperature;
// 省略
  }
}

2.TemperatureInputが自身の温度を更新したい場合は、this.props.onTemperatureChangeを呼び出す。(名前は任意)

second.js
class TemperatureInput extends React.Component{
// 省略
  handleChange(e){
	// this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
  }
// 省略
}

onTemperatureChangeプロパティは親コンポーネントCalculatorからtemperatureプロパティと共に渡される。親コンポーネントは入力の変化に応じて自身のローカルstateを更新し、両方の入力フォームは新しい値で再レンダーされる。

3.現時点のtemperaturescaleの入力を、このコンポーネントのローカルstateに保存する。(両方の入力の保存は不必要)

liftup.js

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}

function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e){
    this.props.onTemperatureChange(e.target.value);
  }

  render(){
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={temperature}
          onChange={this.handleChange}
        />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {
      temperature: '',
      scale: 'c'
    };
  }

  handleCelsiusChange(temperature){
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature){
    this.setState({scale: 'f', temperature});
  }

  render(){
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenehit) : temperature;
    return(
      <div>
        <TemperatureInput
          scale='c'
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange}
        />
        <TemperatureInput
          scale='f'
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange}
        />
        <BoilingVerdict celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

これで、どちらの入力コンポーネントを編集したかに関係なく、Calculatorthis.state.temperaturethis.state.scaleが更新される。

上記の変更処理の流れ

DOMのinputでのonChangeで呼ばれた関数を実行する。
(TemperatureInputコンポーネントのhandleChange)
 ↓
TemperatureInputコンポーネントのhandleChangeはthis.props.temperature()に値を与えて呼び出す。
(onTemperatureChangeを含むpropsは、親クラスのCalculatorから与えられる)
 ↓
編集したフィールドによって、呼ばれるメソッドが決まる。
(CalculatorコンポーネントのhandleCelsiusChangeかhandleFahrenheitChange)
 ↓
このメソッドはCalculatorコンポーネントが新しい入力値と更新した値をthis.setStateに与えて呼び出し、Calculatorコンポーネントを再レンダリングする。
(Calculatorコンポーネントのrender)
 ↓
Calculatorコンポーネントのrenderを呼び出し、両方の入力コンポーネント値を再計算。
(toCelsius、toFahrenehit)
 ↓
Calculatorコンポーネントが与えた新しいpropsで各TemperatureInputコンポーネントのrenderメソッドが呼ばれる。
+
BoilingVerdictのコンポーネントのrenderメソッドが呼ばれる。
 ↓
以上の判定結果と入力コンポーネント値によってDOMを更新する。
+
変更された入力コンポーネントは現在の値によって、もう一方の入力コンポーネントは変換され更新される。

11.コンポジションと継承

子要素の出力

sidebarやDialogのような子要素を知らない汎用的な入れ物を表すコンポーネントで使用される。

child.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Hello React
      </p>
    </Fancyborder>
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

上記のFancyBorderタグの要素はchildrenpropsで渡される。

一般的ではないが、childrenの代わりに独自のpropsを作成して、渡すこともできる。

orgChild.js
function Contacts(){
  return(
    <div className="Contacts">
      <p>about contact</p>
    </div>
  );
}

function Chat(){
  return(
    <div className="Chat">
      <p>chat</p>
    </div>
  );
}

function SplitPane(props){
  return(
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App(){
  return(
    <SplitPane
      left={<Contacts />}
      right={<Chat />}
    />
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('train')
);

Reactの要素はオブジェクトなのでpropsとして渡すことができる。

特化したコンポーネント

コンポーネントは他のコンポーネントを特別な要素として扱うことができる。(以下はDialogに対してWelcomeDialogは特別なケース)

specializedComponent.js
function Dialog(props){
  return(
    <div className="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Dialog
      title="welcome"
      message="hello react"
    />
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

コンポジションはクラスとして定義されたコンポーネントでも同じように動作する。

specializedClassComponent.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function Dialog(props){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </Fancyborder>
  );
}

class SignUpDialog extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = (
      {login: ''}
    );
  }

  handleChange(e){
    this.setState(
      {login: e.target.value}
    );
  }

  handleSignUp(){
    alert(`welcome a board ${this.state.login}`);
  }

  render(){
    return(
      <Dialog
        title="XYZ Program"
        message="How should we refer to you?"
      >
        <input
          value={this.state.login}
          onChange={this.handleChange}
        />
        <button onClick={this.handleSignUp}>
          Sign me up.
        </button>
      </Dialog>
    );
  }
}

ReactDOM.render(
  <SignUpDialog />,
  document.getElementById('train')
);

継承はどうするの

propsとコンポジションにより、コンポーネントの見た目、振る舞いを明示的にそして安全にカスタマイズするのに十分な柔軟性を得ることができる。コンポーネントはどのようなpropsでも受け付け、それはプリミティブ値、React要素、関数でもいい。

コンポーネント間で非UI機能を再利用したい場合、それを別の JavaScriptモジュールに抽出することがいい。
コンポーネントはその関数やオブジェクト、クラスなどを継承することなくインポートすることで使用することができる。

12.Reactの流儀

こちらからどうぞ

最後に

だらだらと長くなってしまいましたが、なんとか推敲することができたかなと...汗
間違っている!気になる!等ありましたら、
暖かい目で編集リクエストをいただければと思います。

参考

React - MAIN CONCEPT

11
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?