Practice of Programming

プログラム とか Linuxとかの話題

React事始め

もはや、無数にありそうな話題なので、あまり公に書く意味はないですが、記録しとかないと忘れちゃうので(年齢的な問題で)。

下記の2つのドキュメントを見ながら進めていきますが、主にガイドの方の内容になります。 ja.reactjs.org ja.reactjs.org

Vue.jsはちょっと触ったことありますが、Reactは初めて触ります。

その前に、nvm のインストール

PC変えてから、まともにnode使ってなかったので、nvmからインストール

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
source ~/.zshrc 
nvm install --lts 16
nvm use 16

nvm以外にも色々あるようですね。

cam-inc.co.jp

開発準備

ここから、チュートリアルと主にガイドの内容を元に進めます。

npx create-react-app my-app
rm src/*
touch src/index.css

src/index.js に下記の内容を記載

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

これで、下記で、http://localhost:3000 がブラウザで開きます。

npm start

ポートを変更したい場合は、

echo "PORT=3001" > .env

のようにして、.env内で、PORTを指定してやればよいです。

Hello, world! を表示する

index.js に下記を追加します。

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = (<h1>Hello, world!</h1>);
root.render(element);

下記が表示されました。めでたし。

JXS

先程書いた、<h1>Hello, world!</h1> ですが、JSXというものです。JSXを使う理由は下記のように説明されています。

表示のためのロジックは、イベントへの応答や経時的な状態の変化、画面表示のためのデータを準備する方法といった、他の UI ロジックと本質的に結合したものであり、React はその事実を受け入れます。

マークアップとロジックを別々のファイルに書いて人為的に技術を分離するのではなく、React はマークアップとロジックを両方含む疎結合の「コンポーネント」という単位を用いて関心を分離します。後のセクションでコンポーネントについては改めて詳しく紹介しますが、現時点で JavaScript にマークアップを書くことが気にくわない場合、こちらの議論で考えが改まるかもしれません。

React で JSX を使うことは必須ではありませんが、ほとんどの人は JavaScript コード中で UI を扱う際に、JSX を見た目に有用なものだと感じています。また、JSX があるために React は有用なエラーや警告をより多く表示することができます。

なお、タグの中身がない場合(imgタグなど)は、XMLのような感じで、記述します。

<img src="/path/to/file" />

また、下記のようにJXS内で改行しても問題ありません。

const element = (<h1>
        Hello, {user.name}!
        </h1>
);

JSX内で、JavaScriptを実行する

JSX内では、{} の中に JavaScriptの処理を書くことができます。

const user = {
  name: "ktat"
};

const element = (<h1>Hello, {user.name}!</h1>);

このように変数を参照するだけでも、下記のように、関数を呼び出すこともできます。

function displayName (u) {
  return u.name;
}
const user = {
  name: "ktat"
};

const element = (<h1>Hello, {displayName(user)}!</h1>);
root.render(element);

{} をタグの属性の値の指定に利用する

<div class="name">name を設定したい場合です。

const user = {
  name: "ktat"
};

const element = (<h1 class="{user.name}">Hello, {user.name}!</h1>);
root.render(element);

上記のようにしたら...と思いますが、これだと、そのままの内容がクラス名となってしまいます。

クォートが不要です。下記のようにしましょう。

root.render(<h1 class={user.name}>Hello, {user.name}!</h1>);

無事に、クラスが設定できました。

と、思いましたが、class ではなくて、className と指定するのが、正しいようです。consoleに下記のメッセージが出ました。

react-dom.development.js:86 Warning: Invalid DOM property `class`. Did you mean `className`?
    at h1

なお、クラス名を複数渡したい場合は...、JavaScriptがかけるので、どのようにでもできます。

テンプレートリテラル

className={`${var1} ${var2}`}

関数(スペースで文字列を join する、classNamesという関数を作る)

className={classNames(var1, var2)}

文字列をHTMLとして使いたい

下記のようにすると、

let s = "&lt;";
root.render(<div>{s}</div>);

< が表示されるのではなくて、&lt; がそのまま表示されます。セキュリティのためだと思いますが、あえて、HTMLとして評価したいような場合は、 dangerouslySetInnerHTMLを使います。

    root.render(<span dangerouslySetInnerHTML={{__html: s}}></span>);

インストールが必要ですが、html-react-parser も使えます。

html-react-parser - npm

const htmlParse = require('html-react-parser');

しておいて、下記のように使えば、HTMLとして解釈されます。

let s = "&lt;";
root.render(<div>{ htmlParse(s) }</div>);

JSXとは?

JSXはオブジェクト表現であり、JXSの返すものをReact要素と呼びます。詳しくは、こちらを。

const element = (<h1>Hello, world!</h1>)

は、

const element = React.createElement(
  'h1',
  'Hello, world!'
);

の呼び出しにコンパイルされ、結果として、

const element = {
  type: 'h1',
  props: {
    children: 'Hello, world!'
  }
};

下記のようなオブジェクトになります。これをReact要素と呼びます。

{}で書いたJavaScriptも評価された結果のオブジェクトになります。

レンダーされた要素を更新する

React要素は一度要素を作成すると、子要素や属性を変更することができません。 前述の通り、変数や関数を指定したとしても、すでに評価済みなので、renderの後に、こんなことをしても何も起きません。

root.render(element);
 
setTimeout(() => { user.name = "hogehoge!"; root.render(element) }, 1000)

renderの後に、下記のように新しくReact要素を作って、renderすれば、更新はできます。

root.render(element);
 
setTimeout(() => {
  user.name = "hogehoge";
 
  const element2 = (<h1 className={user.name}>
        Hello, {displayName(user)}!
        </h1>
  );
 
  root.render(element2);
}, 1000);

1秒後に、ktat が、hogehoge に変わるようになります。

しかし、あまりにアレすぎるので、React要素を作るところを関数化してみます(※いずれにしても良い例ではありません)。

React要素を作る部分を関数化する

これで、1秒ごとに、切り替わって、何回目かも表示されるようになりました。

const root = ReactDOM.createRoot(document.getElementById('root'));
 
let n = 0;
function hello() {
    const userName = n++ % 2 == 0 ? 'ktat' : 'world';
    const element = (<h1>
        Hello, {userName}!<br />
        (count: {n})
        </h1>
    );
    root.render(element);
}

setInterval(hello, 1000);

※ root.render を何回も呼び出していますが、普通は、一回しか呼び出さないです。state付きコンポーネントでそれを実現します。と、書いてますね。

なお、Reactでは(というか、Vueとかもですが)、必要な部分のみが更新されるので、上記の例だと、world,ktat の部分、と、count: の右の数字の部分。動的に変更される部分以外は、変更されません。Developer tool でみるとわかります。

コンポーネントとprops

JavaScriptのfunctionを大文字で始めると、コンポーネントとなります。

function Hello(props) {
    return (<h1>
        Hello, {props.name}!
        </h1>
    );
}

これは、JSXで下記のように使えます。

const element = <Hello name="ktat" />;
root.render(element);

することで、以前と同様、Hello, ktat! が表示されます。

コンポーネント化したことで、下記のように名前を変えて、同じものを複数表示するのも簡単です。

const element = (
        <div>
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
        </div>
);
 
root.render(element);

なお、propsは読み取り専用なので、変更してはいけません。

※最初、下記のように記載したのですが、エラーになりました。理由は追っていません。

const element = (
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
);

state とライフサイクル を使う

state を使うためには、関数コンポーネントをクラスに変更する必要があります。

前述の、

function Hello(props) {
    return (<h1>
        Hello, {props.name}!
        </h1>
    );
}

こちらは、下記のように書いても等価となりますが、下記の形式で記載する必要があります。

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

これに、state を導入します。

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>;
  }
}

このままだと、時間が表示されるだけで、最初に表示されたままで変わりません。

マウント・アンマウント時にメソッドを実行する

Reactでは、DOMが描画されることをマウントといい、DOMが削除されるときをアンマウントといいます。 コンポーネントクラスにcompnentDidMountというメソッドを追加すると、マウントした後(DOMがレンダーされた後)に実行できるようになります。 また、componentWillUnmountというメソッドを追加することで、コンポーネントがアンマウントされて破棄される直前に呼び出されるようになります。

これを利用して、マウント時にtimerをセット、アンマウント前にtimerをクリアする(現在の例ではアンマウントするところがないので、ここでは特に不要といえば不要です)ようにします。 追加するものは、下記になります。

  componentDidMount() {
    this.timerID = setInterval(
      () => this.setDate(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

componentDidMount内で呼び出されているsetDateは、下記のようなもので、this.setStateを呼び出しています。これによって、Reactがstateが変更されたことがわかり、renderを再呼び出しします。

  setDate() {
    this.setState({ date: new Date() });
  }

これで、時間が動的に変わるようになりました。全体のコードは、下記のようになります。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));


class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.setDate(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  setDate() {
    this.setState({ date: new Date() });
  }

  render() {
      return <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>;
  }
}

const element = (
        <div>
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
        </div>
);

root.render(element);

注意事項

setStateを必ず使う

this.state = { date: new Date() };

のように最初に定義しているのだから、setDate() でも、同様にすれば良いと思うかもしれませんが、setState を利用しないと、再Renderはされません。なので、必ず、stateを更新する際は、setStateを使います。

this.state に直接代入できるのは、constructorのみです。

this.propsthis.state は非同期に更新される

ここの例のままですが、下記のようにするのは、NGで、

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

下記のように、setStateに関数を渡す形にしないといけません。

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

setState での更新はマージされる

this.state = { a: 1, b: 2, c: {d: 1} };

のように、constructorで定義したとして、

setState(c:{e: 2});

のように更新すると、下記のように変更されます。

{
  a: 1,
  b: 2,
  c: {e: 2}
}

変更しなかったa,bは、そのままですが、変更されたc はまるっと変更({d: 1} => {e: 2})されます。

イベント処理

下記のように書くことで、aタグをクリックした際に、alertが表示されるようになります。

function  clickAtag() {
  alert(1);
}

const element = (
        <div>
          <a href="https://exapmle.com/" onClick={clickAtag}>click</a>
        </div>
);

root.render(element);

このままだと、https://example.com/に移動しますが、移動したくない場合は、falseを返すのではなくて、preventDefaultを使います。

function  clickAtag(e) {
  e.preventDefault();
  alert(1);
}

ここで使われている、eは、合成イベントと呼ばれるもので、ブラウザの互換性は気にしなくて良いとのことです。

Reactでは、addEventListenerを使うのではなくて、最初に要素を作成する際に、リスナを指定するとのことです。

コンポーネントクラスの中に書く

その場合は、下記のようにします。先に使った例のものに、入れます。

  render() {
      return (<div>
                <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>
                <a href="https://exapmle.com/" onClick={this.clickAtag}>click</a>
              </div>);
  }
  clickAtag(e) {
    e.preventDefault();
    alert(1);
  }

イベント処理でstateを更新する

特に難しいことはないはずですね。今回、componentDidMount とかは不要なので削除しています。

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  clickAtag(e) {
    e.preventDefault();
    this.setState({ date: new Date() });
  }

  render() {
      return (<div>
                <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>
                <a href="https://exapmle.com/" onClick={this.clickAtag}>click</a>
              </div>);
  }
}

ですが、エラーになります。clickAtag内で、this が使えません。 下記のようにする必要があります。

                  <a href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>

これで、クリックするたびに時刻が変わるようになりました。

条件付きレンダー

これは、普通にif とか使ってるだけなので、特別なことはなさそうです。 例えば、時刻の末尾が0と5のときだけ、赤くしたいなら、

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  clickAtag(e) {
    e.preventDefault();
    this.setState({ date: new Date() });
  }

  render() {
      let t = this.state.date.toLocaleTimeString();
      let c = t.match(/[05]$/) ? "color: red" : "color: black";
      return (<div>
                <h1>Hello, {this.props.name} at {t}</h1>
                <a style={{color: c}} href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>
              </div>);
  }
}

※ styleについても、全体を文字列として"color: red" のようには渡せないようです。

レンダーさせたくない

なお、rednerさせたくない場合は、nullを返すようにします。

以下のようにすると、0秒と5秒のときにクリックする(か、最初のrender時が0秒か5秒なら)と、コンポーネントはなくなってしまいます。

        if (t.match(/[05]$/)) {
            return null; 
        }

インラインの条件

下記のように、JSX内に条件を書くこともできます。

  render() {
      let t = this.state.date.toLocaleTimeString();
      return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                 <a style={{color:  t.match(/[05]$/) ? "red" : "black" }} href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>
              </div>);
      }
  }

&&|| も使えますが、falseな場合でも、条件の左の結果自体は、レンダリングされますので注意してください。

        let t = this.state.date.toLocaleTimeString();
        let n = 0
        return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                { n  && "not 0"}
              </div>);
        }

0が表示されてしまいます。

true/falseであれば、renderingされないので、結果が欲しくなければ、下記のようにtrue/falseが返るようにしてやればよいです。

        let t = this.state.date.toLocaleTimeString();
        let n = 0
        return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                { n !== 0  && "not 0"}
              </div>);
        }

一度変数に入れる

以下のように、一度変数に入れてもOKです。

  let term = 1;
 let component = "";
  if (term === 1) {
     component = <h1>aaa</h1>
  }
  return (<div>{component}</div>)

リストとkey

リストを使って、同じコンポーネントを複数表示させましょう。

  render() {
        let t = this.state.date.toLocaleTimeString();
        let names = ["World", "Internet"]
        let element = names.map((name) => <h1>Hello, {name}</h1>)
        return (<div>{element}</div>);
  }

ちゃんと表示されますが、consoleにwarningがでます。

Warning: Each child in a list should have a unique "key" prop.

素直にしたがって、keyとして同じものを与えてみます。

  render() {
        let t = this.state.date.toLocaleTimeString();
        let names = ["World", "Internet"]
        let element = names.map((name) => <h1 key={name}>Hello, {name}</h1>)
        return (<div>{element}</div>);
  }

これで、warningは消えます。

keyは要素の変更、追加、削除等をReactが識別ために使うということのようで、ユニークなID(兄弟要素の中でユニーク。グローバルでユニークでなくて良い)を使うべきです(別にHTMLのタグ内のidとかになるわけではないので、あくまで内部で管理されているはず)。

具体的にどう使うかは、まだ不明。

フォーム

フォームは、ユーザーが変更可能なものなので、そのままだとReactの制御下にならないのですが、valueにstateを入れることで、制御されたコンポーネントとすることができます。

class Hello extends React.Component {
  constructor(props) {
      super(props);
      this.state = { nValue: "", rValue: "", tValue: "" };
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
  }

  handleTextArea(e) {
    this.setState({tValue: e.target.value});
  }
 
  handleRadio(e) {
          console.log(e);
    this.setState({rValue: e.target.value});
  }
 
  render() {
        return (<div>
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                <ul>  
                  <li><input type="radio" name="r" value="1" onChange={this.handleRadio.bind(this)} />1</li>
                  <li><input type="radio" name="r" value="2" onChange={this.handleRadio.bind(this)} />2</li>
                  <li><input type="radio" name="r" value="3" onChange={this.handleRadio.bind(this)} />3</li>
                  <li><input type="radio" name="r" value="4" onChange={this.handleRadio.bind(this)} />4</li>
                </ul>
                <textarea value={this.state.tValue} onChange={this.handleTextArea.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                rValue: {this.state.rValue}<br />
                </form>
                </div>);
  }
}

このようにすると、入力されたテキストや、選択された値が、下記の部分に動的に入ることになります。

                nValue: {this.state.nValue}<br />
                rValue: {this.state.rValue}
                tValue: {this.state.tValue}

他のformタグや複雑な使い方については、ドキュメントを読みましょう(そろそろ疲れてきた)

ちなみに、bind(this)をいっぱい書いてますが、constructorで下記のようにすれば、

  constructor(props) {
      super(props);
      this.state = { nValue: "", rValue: "", tValue: "" };
      this.handleRadio = this.handleRadio.bind(this); // こう書いておく
  }

としておけば、下記のように書くだけでOKです(じゃぁ、そう書けよっていう)。

{this.handleRadio}

stateの共有(リフトアップ)

説明は割愛していますが、state は各コンポーネントで独立していますので、一方で変更したものは、他方ではわかりません。 ですが、共有したいようなときもあるので、その場合は、以下のようにします。

2つのコンポーネントで入力されたものを別のコンポーネントで足し合わせるといったものとか。

そろそろ、疲れてきて、ドキュメントの通りにやっていないんですが、下記な感じでできますね。良いのかな。

lass Hello extends React.Component {
  constructor(props) {
      super(props);
      this.name = props.name
      this.state = { nValue: ""};
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
    this.props.state.v[this.name] = e.target.value;
    this.props.change();

  }

  render() {
    return (
                <div> 
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                a: {this.props.state.v.a}<br />
                b: {this.props.state.v.b}<br />
                </form>
                </div>
    );
  }
}

class HelloSum extends React.Component {
  constructor(props) {
      super(props);  
      this.state = { v: {a: "", b: ""} };
  }
  changeState() {
      this.setState({});
  }

  render() {
        return (<div>
                <Hello name="a" state={this.state} change={this.changeState.bind(this)} />
                <Hello name="b" state={this.state} change={this.changeState.bind(this)} />
                mixed: {this.state.v.a + this.state.v.b}
                </div>);
  }
}

const element = (
        <div>
          <HelloSum name="" />
        </div>
);

root.render(element);
  1. HelloSumコンポーネントから、Helloに対して、自身のstateと、setStateするだけの関数をpropsとして渡す。
  2. Helloでは、textが変更されたら、handleText内で、this.props.stateを更新し、this.props.change()を呼び出して、空のsetState()をすることで、Renderする

ドキュメントだと、こんな感じな気がする。

class Hello extends React.Component {
  constructor(props) {
      super(props);
      this.name = props.name
      this.state = { nValue: "" };
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
    this.props.change(this.name, e.target.value);

  }

  render() {
    return (
                <div>
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                a: {this.props.a}<br />
                b: {this.props.b}
                </form>
                </div>
    );
  }
}

class HelloSum extends React.Component {
  constructor(props) {
      super(props);  
      this.state = { v: {a: "", b: ""} };
  }
  changeState(name, v) {
      let d = this.state;
      d.v[name] = v
      this.setState(d);
  }

  render() {
        return (<div>
                <Hello name="a" a={this.state.v.a} b={this.state.v.b} change={this.changeState.bind(this)} />
                <Hello name="b" a={this.state.v.a} b={this.state.v.b} change={this.changeState.bind(this)} />
                mixed: {this.state.v.a + this.state.v.b}
                </div>);
  }
}

const element = (
        <div>
          <HelloSum name="" />
        </div>
);

root.render(element);
  1. props として、親コンポーネントのstateと、状態を変更するための関数を渡す
  2. コンポーネントでは、propsからもらっている値を表示しておく
  3. コンポーネントから入力されたら、親から渡された関数を呼んで、setStateが呼ばれて更新される

コンポジション vs 継承

継承は不要ということのようです。

ja.reactjs.org

React の流儀

後は、読み物なので、良いでしょう。

ja.reactjs.org

終わり

試しつつ、Blog書きながらというところもありますが、5-6時間かかったかな。疲れた。

参照

途中で言及していた、classNameとか、styleのJSXの記法とかは、下記を見ておく必要がありそうですね。