ラベル React の投稿を表示しています。 すべての投稿を表示
ラベル React の投稿を表示しています。 すべての投稿を表示

2021年9月1日水曜日

React:React + TypeScript

1.関数コンポーネント

関数コンポーネントです。普通の関数と同じく、引数を型づけしてください。
JSXの要素を返せば戻り値は推論されます。

type AppProps = { message: string }; // interfaceでもよい

const App = ({ message }: AppProps) => <div>{message}</div>;

戻り値を型づけする場合には、JSX.Elementです。
正しい値が返されなければ、エラーが示されます。

const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;

型エイリアスやインタフェースは定めずに、型注釈を直接書き加えても構いません。
型をほかで使わないときは、この方が簡単です。

const App = ({ message }: { message: string }) =>

<div>{message}</div>;

引数と戻り値をそれぞれ定めるのでなく、関数そのものに型づけすることもできます。

  • コンポーネントの関数をReact.FunctionComponentまたはReact.FCで型づけするのは、TypeScriptの基本テンプレートから除かれるからです。
  • React 18ではReact.FunctionComponentが、デフォルトではchildrenを暗黙で受け取らないように改められるので、将来的にはVoidFunctionComponentは推奨されなくなります。

2.フック

2.1.useState

useStateフックには、可能なかぎり初期値を与えましょう。そうすれば、状態変数の型は推論されるからです。

const [text, setText] = useState('');  // 初期値を与える

初期値を与えなかったとき、「React: フックで値を定めた要素に制御されたコンポーネントを使うよう警告が出る ー Warning: Changing uncontrolled input」という問題も確認されています。

型を組み合わせたいという場合には、useStateにユニオン型をジェネリックで定めればよいでしょう。

const [id, setId] = useState<string | number>(0);

あるいは、型が定めてあっても、初期値があとの初期化処理で決まるとか、値がない場合としてnullを含めたいということもあるかもしれません。そのときも、ユニオン型でnullを加えてください。

const [user, setUser] = React.useState<IUser | null>(null);

// 初期化処理など

setUser(newUser);

######################################

Genericsの簡単な具体例(関数編)

// number型

function test(arg: number): number {

  return arg;

}

// string型

function test2(arg: string): string {

  return arg;

}

test(1); //=> 1

test2("文字列"); //=> 文字列


これをGenericsを使用する事で下記のように書く事が可能です。

test<string>の引数はstring型だけ、またtest<number>の引数はnumber型だけが許されるようになります。

function test<T>(arg: T): T {

  return arg;

}

test<number>(1); //=> 1

test<string>("文字列"); //=> 文字列

//※ Genericsでも型推論ができるので、引数から型が明示的にわかる場合は省略が可能

test("文字列2"); //=> "文字列2"

######################################

nullは型に含めず、useStateの初期値が決まらないという場合には、型アサーションで逃げる手もあります。

const [user, setUser] = React.useState<IUser>({} as IUser);

// 型 'null' の引数を型 'SetStateAction<IUser>' のパラメーターに割り当てることはできません。

// setUser(null);  // nullは認められない

setUser(newUser);  // 型に合致した値のみ設定できる

ただし、型アサーションはTypeScriptに値({})の型を偽っているだけだ、ということにご注意ください。状態変数(user)の値を正しく扱うことは、コードの書き手に委ねられるのです。誤ればランタイムエラーになってしまうかもしれません。

2.2.useReducer

useReducerフックを使うには、リデューサの関数が定められていなければなりません。関数に型づけするのは、リデューサが受け取る状態(state)とアクション(action)のふたつの引数です。戻り値の型は推論されます。

type STORE = { count: number };

type ACTIONTYPE =

    | { type: 'increment' }

    | { type: 'decrement' };

const reducer = (state: STORE, action: ACTIONTYPE) => {

    switch (action.type) {

        case 'increment':

            return { count: state.count + 1 };

        case 'decrement':

            return { count: state.count - 1 };

        default:

            return state;

    }

};


export default reducer;

すると、useReducerフックを使う側では、とくに型の定めはなくて構いません。

import { useReducer } from 'react';

import reducer from './reducer';


const initialState = { count: 0 };

const Counter = () => {

    const [state, dispatch] = useReducer(reducer, initialState);


};

リデューサ関数そのものを、React.Reducerで型づけることもできます。状態(state)とアクション(action)を受け取って、戻り値は状態です。引数の型はジェネリックで与えてください。

import React from 'react';


// const reducer = (state: STORE, action: ACTIONTYPE) => {

const reducer: React.Reducer<STORE, ACTIONTYPE> = (state, action) => {


};

なお、「React + TypeScript: useReducerを使ったカウンターのアプリケーションに型づけする」は、簡単なカウンターの作例をもとに、さらに詳しく解説しました。ご興味がありましたらお読みください。


2.3.判別可能なユニオン型

判別に用いるプロパティが備わった型を複数結ぶと、判別可能なユニオン型(discriminated union)と呼ばれます。

type ACTIONTYPE =

    | { type: 'increment' }

    | { type: 'decrement' };

判別用のプロパティ値が限定されるため、それ以外の値を弾くことができるのです。

const reducer = (state: STORE, action: ACTIONTYPE) => {

    switch (action.type) {

        case 'increment':

            return { count: state.count + 1 };

        case 'decrement':

            return { count: state.count - 1 };

        // 型 '"reset"' は型 '"increment" | "decrement"' と比較できません。

        /* case 'reset':  // エラー

            return { count: 0 }; */

        default:

            return state;

    }

};


2.4.useEffect

useEffectフックでは、はっきりした型づけより、推論される戻り値の型にご注意ください。たとえば、つぎの例です。

useEffect(

     () =>

          window.setTimeout(() => console.log('timeout'), 1000)

     , []

);

アロー関数式=>の本体に波括弧{}なしに1行で書いた文は、そのまま戻り値になります(「アロー関数」の「関数の本体」参照)。ところが、useEffectのコールバック関数は、戻り値はなし(undefined)にするか、「エフェクトのクリーンアップ」関数でなければなりません。そのため、つぎのようなエラーが示されてしまうのです。

Type 'number' is not assignable to type 'void | Destructor'.


クリーンアップ関数がないときは、useEffectのコールバックから値を返さないように注意してください(本体が1行のときは波かっこ{}で括ります)。

useEffect(

    () => {

        window.setTimeout(() => console.log('timeout'), 1000)

    }

    , []

);


2.5.useRef

useRefフックについて、「React+TypeScript Cheatsheets」には型の定めと初期値の与え方で、3つの例が示されています。currentプロパティが書き替えられるか、null値の代入を認めるかどうかが違いです。

// 読み取り専用。

const nullRef = useRef<number>(null);

// 指定した型の値で書き替えられる。nullは不可。

const nonNullRef = useRef<number>(null!);

// 型指定にnullを含める。

const nullableRef = useRef<number | null>(null);


3.イベントハンドラ

イベントハンドラのもっとも簡単な定め方は、要素のイベント属性(onChange)に直に書き加えることです。引数のイベント(event)の型は推論されますので、注釈が要りません。

import React, { useState } from 'react';


function App() {

    const [text, setText] = useState('');

    return (

        <div>

            <input type="text"

                value={text}

                onChange={(event) => setText(event.currentTarget.value)}

            />

        </div>

    );

}


ハンドラ関数(handleChange)を分けて定めると、通常の関数と同じように引数と戻り値を型づけすることになるでしょう(戻り値は推論させても構いません)。

function App() {


    const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {

        setText(event.currentTarget.value);

    };

    return (

        <div>

            <input type="text"

                value={text}

                // onChange={(event) => setText(event.currentTarget.value)}

                onChange={handleChange}

            />

        </div>

    );

}

さらに、ハンドラ関数そのものを(この場合FormEventHandlerで)型づけることもできます。定められるのは、前の例と同じく引数と戻り値の型です。けれど、それぞれをアラカルトで決めるのでなく、セットにした関数の型で示す方がより明確になります。

function App() {


    // const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {

    const handleChange: React.FormEventHandler<HTMLInputElement> = (event) => {


}


2021年8月29日日曜日

React:コンポーネントのライフサイクル

1.Reactのコンポーネント

ReactではUIを構成する要素のことをコンポーネント(Component)と呼びます。

具体的にはボタンやヘッダー、コンテナなどのことです。Reactで開発する利点としてはそれらのUIパーツをそれぞれ細かくコンポーネントに分けておくことで、再描画が必要なものだけに更新を走らせてパフォーマンスをチューニングができます。

2.コンポーネントのライフサイクル

ライフサイクルにはまず、大きく分けて3つの期間があります。

それぞれ順に

Mounting、Updating、Unmountingと呼ばれます。

2.1.Mounting

UIにコンポーネントが描画されるまでの準備期間。

使用できるメソッド:

  • constructor()
  • getDerivedStateFromProps()
  • render()
  • componentDidMount()

2.2.Updating

UIにコンポーネントが表示されていて、基本的にユーザーが操作できる期間。

使用できるメソッド:

  • getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

2.3.Unmounting

他のコンポーネントに切り替える前に現在のコンポーネントを破棄するための期間。

使用できるメソッド:

  • componentWillUnmount()

3.ライフサイクルメソッド(基本編)

3.1.constructor()

Mounting時

JSXおよびTSXのフォーマットではまず見かけません。JavaScriptでReactを書く場合にstateの初期化やactionのバインドのために使用します。


3.2.componentDidMount()

Mounting時

1度目のrenderが呼ばれた後に1度だけ呼ばれるメソッドです。この時点ではまだUIに表示されていません。データをフェッチしたり、アニメーションやタイマーをセットする場合はここで行います。このメソッドからはDOMが作成されていますが、直接のDOM操作などライフサイクルを外れる処理は原則避けましょう。


3.3.render()

Mounting/Updating時

コンポーネントの根幹となる、一番呼ばれるかつ必須のメソッドです。ここに書いてあるコードが実際にUIに現れるものになります。

renderはpropsやstateが更新されるたびに呼ばれるため、ここで直接propsやstateを操作する処理を書いてはいけません。新しい関数を定義するのも避けましょう。そしてAPIのように、propsやstateの値が変わっても結果は冪等であるべきです。

※冪等:べきとうは、大雑把に言って、ある操作を1回行っても複数回行っても結果が同じであることをいう概念です


3.4.componentDidUpdate()

Updating時

第一引数に1つ前のprops、第二引数に1つ前のstate、第三引数にsnapshot(後述のgetSnapshotBeforeUpdateの返り値)が入ってきますが、必要なければ省略可能です。これもよく呼ばれるメソッドなため、パフォーマンスを低下させるpropsやstateを更新する処理は最低限にしましょう。また、if文や後述のshouldComponentUpdateで無駄な処理を避けることができます。


3.5.componentWillUnmount()

Unmounting時

現在のコンポーネントを破棄する直前に呼ばれるメソッドで、アニメーションやタイマーを設定していた場合はここで破棄します。そうしないと新しいコンポーネントのサイクルが始まった後も、その分のメモリが開放されないままになってしまいます。ちなみにもうrenderが呼ばれることはないので、ここでpropsやstateを変更しても意味がありません。

4.ライフサイクルメソッド(上級編)

4.1.getDerivedStateFromProps()

Mounting時

これだけはstaticです。第一引数に次のprops、第二引数に前のstateが入ってきます。propsの値によってstateの値を更新したい場合、propsが更新されてrender()が呼ばれる前にstateの更新が必要かどうかをチェックするメソッドです。更新があれば更新後のstate、なければnullを返します。


4.2.shouldComponentUpdate()

Updating時

第一引数に次のprops、第二引数に次のstateを持っており、前のpropsやstateの値やアドレスと比較し、変更がなければfalseを、あればtrueを返します。falseが返るとこのあとのrender()は呼ばれず再描画は行われません。PureComponentを使うと自動的にこの比較を行ってくれるため、自分で書くケースは稀ですが要のメソッドです。


4.3.getSnapshotBeforeUpdate()

Updating時

このメソッドの返り値はcomponentDidUpdateの第三引数に渡ります。具体例に挙げられているようなスクロール位置など、引き継ぎたい値がある場合に使います。


また上記に上げた以外で、エラーをハンドリングしたい場合はcomponentDidCatch()というメソッドが用意されています。

5.使用しないライフサイクルメソッド

補足として、React v16.2までで使用されていて、現在は使用を控えたほうがいいメソッドもここで上げておきます。古い資料ではこれらの記述がありますが、見かけても読み飛ばしてよいです。


5.1.getDefalutProps()

一度だけ呼ばれ、propsが渡されなかった場合に使う値を指定できます。constructorや、TypeScriptではデフォルト値を指定することで解決可能。


5.2.getInitialState()

Mounting時にまず呼ばれ、描画前にstateを変更するためのメソッド。TypeScriptではstateに初期値を指定できるので特に必要性を感じることはありません。


5.3.componentWillMount()

v16.3以降ではUNSAFE_componentWillMount()に改名されており、v17で完全に削除予定。1回目のrenderが呼ばれる直前に実行されます。


5.4.componentWillUpdate()

v16.3以降ではUNSAFE_componentWillUpdate()になっており、これもv17から削除予定です。使いたいと思ったら代わりにcomponentDidUpdate()やgetSnapshotBeforeUpdate()を使用すべき。


5.5.componentWillReceiveProps()

v16.3以降ではUNSAFE_componentWillReceiveProps()で、v17から削除予定。componentDidUpdate()またはgetDerivedStateFromProps()で代用できます。


2021年8月26日木曜日

React:開発手順

 1.準備

簡単なデザインと、JSONデータを準備する。
設計書もよいが、テスト駆動型でかつアジャイルで開発するほうが、よりよいものができる

2.UI をコンポーネントの階層構造にする

モックを形作っている各コンポーネント(構成要素)を四角で囲んで、それぞれに名前をつけていくことです。

JSON のデータモデルをユーザに向けて表示することはよくありますので、モデルを正しく構築されていれば、UI(つまりコンポーネントの構造)にもうまくマッピングされるということが分かるでしょう。これは、UI とデータモデルが同じ 情報の構造 を持つ傾向があるためです。UI を分割して、それぞれのコンポーネントがデータモデルの厳密に一部分だけを表現するよう、落とし込みましょう。

5 種類のコンポーネントがこのアプリの中にあることが見て取れます。

  1. FilterableProductTable(オレンジ色): このサンプル全体を含む
  2. SearchBar(青色): すべてのユーザ入力を受け付ける
  3. ProductTable(緑色): ユーザ入力に基づくデータの集合を表示・フィルタする
  4. ProductCategoryRow(水色): カテゴリを見出しとして表示する
  5. ProductRow(赤色): 各商品を 1 行で表示する

ProductTable を見てみると、表のヘッダ(「Name」や「Price」のラベルを含む)が単独のコンポーネントになっていないことがわかります。

これは好みの問題で、コンポーネントにするかしないかは両論あります。今回の例でいえば、ヘッダを ProductTable の一部にしたのは、データの集合を描画するという ProductTable の責務の一環として適切だったからです。

しかしながら、将来ヘッダーが肥大化して複雑になった場合(例えばソート機能を追加した場合など)は、ProductTableHeader のようなコンポーネントにするのが適切になるでしょう。

さて、モック内にコンポーネントを特定できましたので、階層構造に並べてみましょう。モックで他のコンポーネントの中にあるコンポーネントを、階層構造でも子要素として配置すればいいのです。次のようになります。

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

3.Reactで静的なバージョンを作成する

データモデルを描画するだけの機能を持った静的なバージョンのアプリを作る際には、他のコンポーネントを再利用しつつそれらに props を通じてデータを渡す形で、自分のコンポーネントを組み上げます。

props は親から子へとデータを渡すための手段です。

state はユーザ操作や時間経過などで動的に変化するデータを扱うために確保されている機能です。今回のアプリは静的なバージョンなので、state は必要ありません。

コンポーネントはトップダウンで作っても、ボトムアップで作っても問題ありません。

つまり、高い階層にあるコンポーネント(例えば FilterableProductTable)から作り始めても、低い階層にあるコンポーネント(ProductRow など)から作り始めても、どちらでもいいのです。シンプルなアプリでは通常トップダウンで作った方が楽ですが、大きなプロジェクトでは開発をしながらテストを書き、ボトムアップで進める方がより簡単です。

ここまでのステップを終えると、データモデルを描画する再利用可能なコンポーネントのライブラリが手に入ります。このアプリは静的なバージョンなので、コンポーネントは render() メソッドだけを持つことになります。

階層構造の中で最上位のコンポーネント(FilterableProductTable)が、データモデルを props として受け取ることになるでしょう。元となるデータモデルを更新して再度 ReactDOM.render() を呼び出すと、UI が更新されることになります。このやり方なら、複雑なことをしていないので、UI がどのように更新されて、どこを変更すればよいか、理解できることでしょう。React の単方向データフロー(あるいは単方向バインディング)により、すべてがモジュール化された高速な状態で保たれます。

4.UI 状態を表現する state を決定

UI をインタラクティブなものにするためには元となっているデータモデルを更新できる必要があります。これは React なら state を使うことで実現できます。


適切に開発を進めていくにあたり、そのアプリに求められている更新可能な状態の最小構成を、最初に考えておいたほうがよいでしょう。

ここで重要なのは、DRY (don’t repeat yourself)の原則です。アプリケーションが必要としている最小限の状態を把握しておき、他に必要なものが出てきたら、そのとき計算すればよいのです。

例えば、TODO リストを作る場合、TODO の各項目を配列で保持するだけにし、個数のカウント用に別の state 変数を持たないようにします。その代わりに、TODO の項目数を表示したいのであれば、配列の length を使えばよいのです。


今回は、次のようなデータがあるとする

  • 元となる商品のリスト
  • ユーザが入力した検索文字列
  • チェックボックスの値
  • フィルタ済みの商品のリスト

それぞれについて見ていき、どれが state になりうるのかを考えてみます。

各データについて、考えましょう。

  • 親から props を通じて与えられたデータでしょうか?
    もしそうなら、それは state ではありません
  • 時間経過で変化しないままでいるデータでしょうか?
    もしそうなら、それは state ではありません
  • コンポーネント内にある他の props や state を使って算出可能なデータでしょうか? もしそうなら、それは state ではありません
例で考えると

  • 元となる商品のリストは props から渡されるので、これは state ではありません。
  • 検索文字列とチェックボックスは時間の経過の中で変化し、また、算出することもできないため、state だと思われます。
  • 最後に、フィルタ済みの商品のリストは state ではありません。何故ならば、元となる商品のリストと検索文字列とチェックボックスの値を組み合わせることで、フィルタ済みの商品のリストを算出することが可能だからです。

というわけで、state と呼べるのは次の 2 つです。

  • ユーザが入力した検索文字列
  • チェックボックスの値


5.state をどこに配置すべきか

アプリの各 state について、次の各項目を確認していきます。

  • その state を使って表示を行う、すべてのコンポーネントを確認する
  • 共通の親コンポーネントを見つける(その階層構造の中で、ある state を必要としているすべてのコンポーネントの上位にある単一のコンポーネントのことです)
  • 共通の親コンポーネントか、その階層構造でさらに上位の別のコンポーネントが state を持っているべきである
  • もし state を持つにふさわしいコンポーネントを見つけられなかった場合は、state を保持するためだけの新しいコンポーネントを作り、階層構造の中ですでに見つけておいた共通の親コンポーネントの上に配置する

それでは、例に適用してみましょう。

  • ProductTable は商品リストをフィルタする必要があり、SearchBar は検索文字列とチェック状態を表示する必要がある
  • 共通の親コンポーネントは FilterableProductTable である
  • 概念的にも、検索文字列とチェック状態が FilterableProductTable に配置されることは妥当である

state を FilterableProductTable の中に配置することが決まりました。では早速、インスタンス変数として this.state = {filterText: '', inStockOnly: false} を FilterableProductTable の constructor に追加して、初期状態をアプリに反映しましょう。その次は、filterText と inStockOnly を ProductTable と SearchBar に props として渡します。最後に、これらの props を使って ProductTable のフィルタ処理を行い、SearchBar のフォームにも値を埋めます。


6.逆方向のデータフローを追加

現在のバージョンのサンプルで文字を打ち込んだり、チェックボックスを切り替えてみると、React がその入力を無視することがわかります。これは意図的な挙動で、input の value props が、常に FilterableProductTable から渡された state と同じ値になるようにセットしてあるのです。


それでは、どんな挙動になってほしいのかを考えてみましょう。ユーザがフォームを変更するたびに、ユーザ入力を反映するように state を更新したいですね。コンポーネントの state を更新できるのは自分自身だけであるべきなので、FilterableProductTable は SearchBar にコールバックを渡しておいて、state を更新したいときに実行してもらうようにします。入力のたびに呼び出される onChange イベントを利用するとよいでしょう。このコールバックを実行された FilterableProductTable は、setState() を呼び出し、その結果としてアプリが更新されます。


2021年8月25日水曜日

React:デザインパターン

1.デザインパターンの重要性

デザインパターンとは、ソフトウェア開発において一般的に発生する問題に対する反復可能な解決策のことです。

デザインパターンは基本的なテンプレートとして機能し、与えられた要求に応じてプログラムの機能を構築することができます。


デザインパターンは、開発プロセスを高速化するだけでなく、コードの読みやすさやメンテナンスのしやすさを向上させます。

デザインパターンの一般的な例としては、SingletonパターンやGang-of-Fourパターンなどがあります。


ソフトウェア開発において、デザインパターンには2つの共通した役割があります。

  • 開発者に共通のプラットフォームを提供する

デザインパターンは、標準的な用語や既知の問題に対する解決策を提供する。

  • ベストプラクティスの確保

デザインパターンは、広範な研究とテストの結果として作成されています。デザインパターンは、開発者が開発環境に容易に慣れることができるだけでなく、ベストプラクティスが守られていることを保証します。

その結果、エラーが少なくなり、適切なデザインパターンが実装されていれば簡単に回避できたであろう問題のデバッグや解明にかかる時間を節約することができます。


2.Reactjsの機能

reactは、Githubで公開されているcreate-react-appを使ってインストールできます。npmを使用すると、他のすべての依存関係を追加できます。

React.jsは、JSXを利用しています。これは、JavaScriptの構文拡張です。JavaScriptのフルパワーを備えており、Reactの「要素」と呼ばれるものを提供してくれます。


JSXの使用は必須ではありませんが、JSXが提供する便利な視覚支援とスタイリングオプションのため、好ましい方法です。また、有用なエラーメッセージや警告も表示されます。

React.jsの基本理念は、再利用可能なリアクトコンポーネントです。このコンポーネントベースのアプローチは、Webアプリケーションのリッチなユーザーインターフェースを構築するために活用できることがわかります。

これらのReactコンポーネントは、それ自体が小さなシステムであると考えることができます。各コンポーネントは、独自の状態、入力、出力を持ちます。

コンポーネントの入力は、プロップの形で取得されます。コンポーネントは、ブラックボックスと考えることができます。それぞれが独自の状態とライフサイクルを持っています。コンポーネントは簡単に構成することができます。

最終的なReactアプリは、保守性の高いコードで構成されています。

3.Reactのデザインパターン

3.1.ステートレスコンポーネント

ステートとは、コンポーネントにインポートされるデータのことです。通常、データはデータベースから取得されます。

コンポーネントには、ステートフルコンポーネントとステートレスコンポーネントという2つのタイプがあります。両者の違いは、単にステートがあるかないかだけです。

ステートレスコンポーネントでは、その内部でthis.stateに到達することはできません。

ステートレスコンポーネントは、機能的コンポーネントや提示的コンポーネントとも呼ばれます。Reactでは、このようなコンポーネントは常に同じものをレンダリングするか、propsで渡されたものだけをレンダリングします。

開発者としての目的は、たとえその特定のコンポーネントを再利用しなければならないシナリオがすぐになくても、ステートレスコンポーネントを作成することです。

多くの場合、開発者は、あるコンポーネントが状態を持つ必要があるかどうかを、コードを書き始めてから理解しますが、それは必ずしも事前に明確ではないからです。

階層構造のコンポーネントでは、親コンポーネントにできるだけ多くの状態を保持させ、ステートレスな子コンポーネントを作るのがベストな方法です。データの受け渡しはプロップで行います。


3.2.条件付きレンダリング

条件はソフトウェア開発者の武器の中で最も重要なツールです。

Reactコンポーネントを作成する過程で、状態に応じて特定のJSXコードをレンダリングする必要が生じることがよくあります。これを実現するのが、条件付きレンダリングです。

条件付きレンダリングは、ニーズに応じて個別のコンポーネントを作成し、アプリケーションに必要なものだけをレンダリングすることができるので、非常に便利です。

例えば、条件付きレンダリングを使って、ユーザーのログイン状態に応じて異なるメッセージをユーザーに表示することができます。メッセージは、 isLoggedIn  というプロップの値に従います。


3.3.レンダリングプロップ

デザインパターンが共通の問題を解決するために存在することを説明しました。Reactでは、ロジックの繰り返しの問題を解決するためにRenderプロップが用意されています。

Reactの公式ドキュメントによると、レンダープロップは「値が関数であるプロップを使ってReactコンポーネント間でコードを共有するためのテクニック」と定義されています。

レンダープロップは、異なるコンポーネント間で同じ状態を共有することができるため、非常に便利なものです。各コンポーネント内のロジックをハードコーディングする代わりに、何をレンダリングするかを決定するために関数プロップを使用することができます。

Formik、React Router、Downshiftなど、レンダープロップを利用した人気のあるライブラリがあります。


3.4.制御されたコンポーネント

Webフォームは多くのアプリケーションで共通の要件であり、制御されたコンポーネントはフォームの状態を処理するためのReactの答えです。

コントロールされたコンポーネントは、プロップを通して状態を受け取ります。それは  onChange のようなコールバックによって変更を通知することができます。 親コンポーネントは、コールバックを処理し、自身の状態を管理することで、コントロールされたコンポーネントを制御することができ、その間、新しい値はコントロールされたコンポーネントにpropsとして渡されます。デフォルトでは、Reactフォームは制御されたコンポーネントと制御されていないコンポーネントの両方をサポートしています。制御されたコンポーネントを使用することを強くお勧めします。

次のコードスニペットは、制御されたコンポーネントを示しています。

<input type = "text" value = {value} onChange = {handleChange} />    


3.5.Reactのフック

フックはReactの比較的新しい追加機能で、React 16.8で導入されました。

これらの機能により、開発者はクラスなしでReactを使用することができます。Effect Hook ( useEffect ) や State Hook など、さまざまなフックがあらかじめ用意されています。

利用可能なフックの完全なリストについては、 Hooks API Reference  を参照してください。 Reactにあらかじめ用意されているフックとは別に、独自のフックを作成することもできます。これにより、コンポーネントのロジックを抽出し、再利用可能な関数を作成することができます。


フックはReactの歓迎すべき機能であり、開発者コミュニティはこの新しい機能を非常に熱心に評価しています。

しかし、フックの引数がオブジェクト、配列、関数である場合には、フックの扱いが少し難しくなることがあることを覚えておく必要があります。これでは少々混乱してしまいます。

一方で、カスタムフックは簡単で使いやすく、開発者にとっても大きなメリットがあります。


React:React

1.Reactとは 

JavaScript フロントエンドフレームワーク

アプリケーションで各状態に対するシンプルな設計をすることで、React はデータが変更された時に、適切なコンポーネントだけを効率的に更新してレンダリングすることができる

自身の状態を管理するカプセル化されたコンポーネントを構築することで、それらのコンポーネントを呼び出し複雑なUI もコンポーネント呼び出しで作成できる


2.Reduxとは

Fluxアーキテクチャの一種

2.1.構成用要素

2.1.1.Reduce

状態を変化させる関数

引数

store        今の状態を示すオブジェクト

action        どんな動作を行ったかを示すオブジェクト


ES2015仕様:Spread Operator

const hoge = [23];
console.log(1, ...hoge, 4.5);

// 実行結果 [1,2,3,4,5]


const fuga = { name: 'Taro', age: 25 };
const piyo = { name: 'Jiro', location: 'Tokyo' };
console.log({ ...fuga, ...piyo });

// プロパティがかぶる場合、後のものが優先する
// 実行結果 {name: 'Jiro',age: 25 ,location: 'Tokyo' }


const myFunc = (x, y, z) => [x, y, z];
const age = [012];
mfFunc(...age);

// 実行結果 [0,1,2]


注意点

値渡が基本 オブジェクトや配列は参照渡しとなるため、Object.assignを用いる

onst target = { a: 1, b: 2}
const source = { b: 3, d: 4}

const returnedTarget = Object.assign(target, source);

console.log(target);
console.log(source);
console.log(returnedTarget);

// $ node test.js
// { a: 1, b: 3, d: 4 }
// { b: 3, d: 4 }
// { a: 1, b: 3, d: 4 }


let user = {
    username: "John",
};

let user_id = {
    id: 1,
};

let email = {
    email: "john@example.com",
};

user = Object.assign(user, user_id, email);

console.log(user);

  //結果
  //{ username: 'John', id: 1, email: 'john@example.com' }


Object.assignを使った例(form)

const form = {
    firstName: null,
    lastName: null,
    Email: null,
    zipCode: null,
    Address: null,
    Phone: null
}

const input = {
    firstName: 'John',
    lastName: 'Doe',
    Email: 'john@example.com'
}

Object.assign(form, input)

console.log(form)

// $ node test.js
//  {
//  firstName: 'John',
//  lastName: 'Doe',
//  Email: 'john@example.com',
//  zipCode: null,
//  Address: null,
//  Phone: null
// }


2021年8月24日火曜日

React:React + Material-UI

ReactとMaterial-UIを利用した管理画面などを作成することが多くなりました

また、前提としてJavaScriptやTypeScriptの知識があるとより分かりやすいです。


1.アプリの作成

Create React Appで新しいReactアプリを作成します。

npx create-react-app react-material-ui-sample --template typescript        

プロジェクトのディレクトリへ移動して実行します。

cd react-material-ui-sample    

npm start    


2.ディレクトリ構成

ディレクトリはあまりネストさせすぎずシンプルな構造にしました。
コンポーネントの分け方はAtomic Designを参考にしています。

3.ルーティング

ルーティングを実装するためのライブラリをインストールします。

npm install --save react-router-dom    

npm install --save-dev @types/react-router-dom    


4.Material-UIを導入

Reactアプリの準備が出来たので、Material-UIを導入していきます。


4.1.Material-UIをインストール

npm install --save @material-ui/core @material-ui/icons    


4.2.フォントを導入

Material-UIと相性の良いGoogle日本語フォントとフォントアイコンを導入しておきます。public/index.htmlのヘッダーにCDNのURLを追加します。

index.html

<head>

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Noto+Sans+JP&subset=japanese" />

    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

</head>

4.3.テンプレートを作成

src/components/templates/にページのテンプレートを作成します。Material-UIが公開しているテンプレートを参考に実装します。

import React from "react";
import clsx from "clsx";
import { createTheme } from "@material-ui/core/styles";
import * as colors from "@material-ui/core/colors";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
import { ThemeProvider } from "@material-ui/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
import Box from "@material-ui/core/Box";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import Container from "@material-ui/core/Container";
import { Link } from "react-router-dom";
import MenuIcon from "@material-ui/icons/Menu";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import IconButton from "@material-ui/core/IconButton";
import HomeIcon from "@material-ui/icons/Home";
import ShoppingCartIcon from "@material-ui/icons/ShoppingCart";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";

const drawerWidth = 240;

const theme = createTheme({
  typography: {
    fontFamily: [
      "Noto Sans JP",
      "Lato",
      "游ゴシック Medium",
      "游ゴシック体",
      "Yu Gothic Medium",
      "YuGothic",
      "ヒラギノ角ゴ ProN",
      "Hiragino Kaku Gothic ProN",
      "メイリオ",
      "Meiryo",
      "MS Pゴシック",
      "MS PGothic",
      "sans-serif",
    ].join(","),
  },
  palette: {
    primary: { main: colors.blue[800] }, // テーマの色
  },
});

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      display: "flex",
    },
    toolbar: {
      paddingRight: 24,
    },
    toolbarIcon: {
      display: "flex",
      alignItems: "center",
      justifyContent: "flex-end",
      padding: "0 8px",
      ...theme.mixins.toolbar,
    },
    appBar: {
      zIndex: theme.zIndex.drawer + 1,
      transition: theme.transitions.create(["width""margin"], {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
    },
    appBarShift: {
      marginLeft: drawerWidth,
      width: `calc(100% - ${drawerWidth}px)`,
      transition: theme.transitions.create(["width""margin"], {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.enteringScreen,
      }),
    },
    menuButton: {
      marginRight: 36,
    },
    menuButtonHidden: {
      display: "none",
    },
    title: {
      flexGrow: 1,
    },
    pageTitle: {
      marginBottom: theme.spacing(1),
    },
    drawerPaper: {
      position: "relative",
      whiteSpace: "nowrap",
      width: drawerWidth,
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.enteringScreen,
      }),
    },
    drawerPaperClose: {
      overflowX: "hidden",
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
      width: theme.spacing(7),
      [theme.breakpoints.up("sm")]: {
        width: theme.spacing(9),
      },
    },
    appBarSpacer: theme.mixins.toolbar,
    content: {
      flexGrow: 1,
      height: "100vh",
      overflow: "auto",
    },
    container: {
      paddingTop: theme.spacing(4),
      paddingBottom: theme.spacing(4),
    },
    paper: {
      padding: theme.spacing(2),
      display: "flex",
      overflow: "auto",
      flexDirection: "column",
    },
    link: {
      textDecoration: "none",
      color: theme.palette.text.secondary,
    },
  })
);

const Copyright = () => {
  return (
    <Typography variant="body2" color="textSecondary" align="center">
      {"Copyright © "}
      <Link color="inherit" to="/">
        管理画面
      </Link>{" "}
      {new Date().getFullYear()}
      {"."}
    </Typography>
  );
};

export interface GenericTemplateProps {
  children: React.ReactNode;
  title: string;
}

const GenericTemplate: React.FC<GenericTemplateProps> = ({
  children,
  title,
}) => {
  const classes = useStyles();
  const [open, setOpen] = React.useState(true);
  const handleDrawerOpen = () => {
    setOpen(true);
  };
  const handleDrawerClose = () => {
    setOpen(false);
  };

  return (
    <ThemeProvider theme={theme}>
      <div className={classes.root}>
        <CssBaseline />
        <AppBar
          position="absolute"
          className={clsx(classes.appBar, open && classes.appBarShift)}
        >
          <Toolbar className={classes.toolbar}>
            <IconButton
              edge="start"
              color="inherit"
              aria-label="open drawer"
              onClick={handleDrawerOpen}
              className={clsx(
                classes.menuButton,
                open && classes.menuButtonHidden
              )}
            >
              <MenuIcon />
            </IconButton>
            <Typography
              component="h1"
              variant="h6"
              color="inherit"
              noWrap
              className={classes.title}
            >
              管理画面
            </Typography>
          </Toolbar>
        </AppBar>
        <Drawer
          variant="permanent"
          classes={{
            paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
          }}
          open={open}
        >
          <div className={classes.toolbarIcon}>
            <IconButton onClick={handleDrawerClose}>
              <ChevronLeftIcon />
            </IconButton>
          </div>
          <Divider />
          <List>
            <Link to="/" className={classes.link}>
              <ListItem button>
                <ListItemIcon>
                  <HomeIcon />
                </ListItemIcon>
                <ListItemText primary="トップページ" />
              </ListItem>
            </Link>
            <Link to="/products" className={classes.link}>
              <ListItem button>
                <ListItemIcon>
                  <ShoppingCartIcon />
                </ListItemIcon>
                <ListItemText primary="商品ページ" />
              </ListItem>
            </Link>
          </List>
        </Drawer>
        <main className={classes.content}>
          <div className={classes.appBarSpacer} />
          <Container maxWidth="lg" className={classes.container}>
            <Typography
              component="h2"
              variant="h5"
              color="inherit"
              noWrap
              className={classes.pageTitle}
            >
              {title}
            </Typography>
            {children}
            <Box pt={4}>
              <Copyright />
            </Box>
          </Container>
        </main>
      </div>
    </ThemeProvider>
  );
};

export default GenericTemplate;

簡単に記載している