Published on

React基礎

Authors
  • avatar
    Name
    Kikusan
    Twitter

Refs

shell

npx create-react-app hello-world --template=typescript
cd hello-world
# eslint グローバルインストールでパスを通す 他にプロジェクトがあれば -> https://qiita.com/mysticatea/items/6bd56ff691d3a1577321
npm install -g eslint
npm i -D eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
eslint --init # 質問に答える .eslintrc.[js/yaml]が生成されるのを書き換えて自分好みにする
eslint src # コードチェック

eslint

読み込みファイルを限定するため.eslintrc.jsparseOptionsproject: "./tsconfig.eslint.json"を設定し、ファイルを以下の内容で作成する。

{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.js",
    "src/**/*.jsx",
    "src/**/*.ts",
    "src/**/*.tsx"
  ], 
  "exclude": [ "node_modules" ]
}

VS Code拡張

チュートリアル

github: https://github.com/kikusan-16/react-tutorial

このチュートリアルはクラスコンポーネントを利用したstateを使用している。
この管理方法ではstateを子孫にpropsとして渡しているが、
state管理の問題としてpropがバケツリレーされるprop drilling問題があり、これを解決するためにreduxがあった。
ただ、今はReact関数コンポーネントにReact Hooksが導入され、状態管理を簡潔に行えるようになった。
今は関数コンポーネントを基本的に使用して、できないことがある場合にクラスコンポーネントをつかうといい。

レンダーとJSX

React要素はrenderによりDOMにレンダリングされる。

JSXの特性

  • コンパイルによってReact.createElement()関数に変換される。(帰ってくるのはObject=React要素)
  • タグを生成できる。{}で変数を代入できる。const element = <h1>Hello, {name}</h1>;
  • デフォルトでXSSを防ぐ。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  // strictモードは本番ビルドには影響を与えない
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

ReactDOMは要素とその子要素に必要な分だけDOMの更新を行う。
コードでJSX全体を塗り替えるように書いていても、差分がある部分だけ更新される。

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

const tick = () => {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2> {/* 更新されるのはここだけ*/}
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);

import React from 'react';はJSXがコンパイルされた後必要になるので必ず記載が必要だったが、
TypeScriptではtsconfig.jsoncompilerOptions.jsx=react-jsxとするとimportしなくても動作するようになる。

コンポーネントとprops

関数コンポーネント

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

// propsの型定義
type Props = {name: String, children?: React.ReactNode};

// TypeScriptでジェネリクス使用
const Welcome: React.FunctionComponent<Props> = (props: Props) => {
  // 分割代入
  const { name, children } = props;
  return <>
           <h1>Hello, {name}</h1>
           {/* childrenは暗黙的に呼び出し時の中身のタグを受け取れる */}
           {children}
         </>;
}

root.render(
  // propsに属性を渡せる
  <Welcome name="Kikusan">
    {/* childrenに子要素を渡せる */}
    <p>this is children</p>
  </Welcome>
);

クラスコンポーネント

クラスコンポーネントではstateとライフサイクルメソッドを使用できる。
Hooksがある今は率先して使う必要はない。


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

type Props = {date?: Date};
type State = {date?: Date};

// <props, state>で型指定
class Clock extends React.Component<Props, State> {
  
  timerID?: number;

  // stateの更新はコンストラクタで
  constructor(props: Props) {
    super(props);
    this.state = {date: new Date()};
  }

  // レンダリングはrenderメソッドで
  render() {
    return <h2>It is {this.state.date && this.state.date.toLocaleTimeString()}.</h2>
  }

  // マウント時一度だけ呼ばれる。データロードはcomponentDidMountで
  componentDidMount = () => {
    this.timerID = window.setInterval(
      () => this.tick(),
      1000
    );
  }

  // 更新があると呼ばれる。stateとpropsの変更はここで。
  componentDidUpdate = () => {
    console.log(this.state);
  }

  // 削除時処理はcomponentWillUnmountで
  componentWillUnmount = () => {
    clearInterval(this.timerID);
  }

  tick = () => {
    // stateの更新はsetStateで行う
    this.setState({
      date: new Date()
    });
  };
}

root.render(<Clock />);

Component Lifecycle

  1. constructor: 1度だけの初期化
  2. render: JSXの返却以外は行わない
  3. (表示される)
  4. componentDidMount: マウントされた時1度だけ呼ばれる。データロードにちょうどいい
  5. (更新を待つ)
  6. componentDidUpdate: 親か自分の要素に変更があった時に呼ばれる。state/propsの変更をするのにちょうどいい
  7. (コンポーネントが表示されなくなるのを待つ)
  8. componentWillUnmount: 通常は使用せずなにかcleanupが必要な場合使用する

if

JavaScriptのifを使用するか、jsxでは{}で真偽値を使用する。

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
const n = Math.floor(Math.random()*10);
root.render(
    <>
      {/* 偽の場合はレンダリングされない */}
      {n > 5 && <p>{n} is larger than 5 </p>}
      <p>{n} is {n % 2 === 0 ? 'even' : 'odd'}</p>
    </>
);

for

配列を使用する。

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

const numbers = [1, 2, 3, 4, 5];
root.render(
  <>
    <ul>
      {/* keyはReactの識別性のために必要。並び替えが変わる場合を考慮して、index以外を使用すること */}
      {numbers.map((number) =>
        <li key={number.toString()}>{number}</li>
      )}
    </ul>
  </>
);

組み込みコンポーネント

予め用意されたタグがJSXでは使える。
基本的にhtmlと同じだが、JavaScriptの命名規則に合わせて属性名が変わっている。

(ユーザ定義コンポーネントでも同じだが)boolean値は値を省略するとtrueになる。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L3140

Reack Hooks

関数コンポーネントでのみ、使える。
ループや条件分岐内ではなく、React関数内トップレベルで使用する。

Hooksにおけるpropsやstateの値はレンダリング毎に固定される。

useState

オブジェクトの状態を保つ。

import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const Example = (): React.ReactElement => {
  // count=変数
  // setCount=関数, setCount(処理)
  // useState(初期値)
  const [count, setCount] = useState(0)
  // hooks関数には関数を渡せる。引数にはstateが入る
  // count+1でも書けるが、hooksではstateの値が常に最新とは限らないので関数を渡す
  const increment = (): void => setCount((c) => c + 1)

  return (
    <div>
      <p>You clicked { count } times</p>
      <button onClick={ increment }>
        Click me
      </button>
    </div>
  )
}
root.render(<Example />)

useEffect

副作用がある場合のオブジェクトの書き換え。componentDidMount and componentDidUpdateに似ている。

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const Example = (): React.ReactElement => {
  const [count, setCount] = useState(0)

  /* useEffect
   * 第二引数は依存配列といい、配列内の変数に差分があると第一引数が実行される。
   * 省略するとレンダリング毎に第一引数が実行される。
   * [] のときはcomponentDidMountと同じように実行される。
   * [count] のときはcomponentDidUpdateと同じように実行される。
   *
   * 正確にはcomponentDidMountは描画前に実行されるが、useEffectは先に初期値で描画される
   * (これを利用してロード中を簡単に表せる)
   */
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`

    // return () => clearSomething() 戻り値に関数を入れるとunMount時に実行
  }, [count])

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
root.render(<Example />)

useMemo

計算量の多い処理を分けておき、依存配列に変化があった場合だけ再計算するようにできる。

import React, { useState, useMemo } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const Example = (): React.ReactElement => {
  const calc = (n: number): number => {
    return n ** 2
  }

  const [count, setCount] = useState(0)

  // 依存配列の中身に変更があった場合だけ再計算
  const memoizedValue = useMemo(() => calc(count), [count])

  return (
    <div>
      <p>{`You clicked ${count} times => ${memoizedValue}`}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
root.render(<Example />)

useRef

Refはレンダリングではなくて要素の変更を行いたい場合に使用する。
リアルDOMの要素を参照できる。

以下の場合に使用できる。

  • フォーカス、テキストの選択およびメディアの再生の管理
  • アニメーションの発火
  • サードパーティの DOM ライブラリとの統合
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

const TextInput: React.FunctionComponent = () => {
  // HooksのuseRef(初期化値)
  const inputRef = React.useRef<HTMLInputElement>(null);
  const onButtonClick = (): void => {
    // current はrefが渡されたリアルDOMオブジェクトになる
    if (inputRef.current) inputRef.current.focus();
  };
  return (
    <div>
      {/* refを渡す */}
      <input type="text" ref={inputRef} />
      <input type="button" value="Focus" onClick={onButtonClick} />
    </div>
  );
};

root.render(<TextInput />);

useContext

コンポーネントへの値の受け渡しを可能にする。
useContext呼び出しオブジェクトから、直近の<ContextName.Provider>が記載された値を取り出す。

import React, { useContext } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee'
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222'
  }
}

// createContext(デフォルト値)でcontextを作成する
const ThemeContext = React.createContext(themes.light)

const App = (): React.ReactElement => {
  return (
    // タグにcontext.Provider value= を使用してツリーの子孫に値を差し替えられる。
    // 子供は直近の親のcontext.Providerに作用される
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  )
}

const Toolbar = (): React.ReactElement => {
  return (
    <div>
      <ThemedButton />
    </div>
  )
}

const ThemedButton = (): React.ReactElement => {
  // useContextでcontextを使用する。
  const theme = useContext(ThemeContext)
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  )
}

root.render(<App />)

useReducer

React Hooksで個別のコンポーネントにおいてReduxの機能を再現したもの。 useStateの代替品として使用され、複数の値にまたがる複雑なstateロジックがある時に使う。

import React, { useReducer } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const initialState = { count: 0 }

const CounterActionType = {
  increment: 'counter/increment',
  decrement: 'counter/decrement'
} as const // as const で全ての値をreadonlyにする

type ValueOf<T> = T[keyof T] // Tのvalueのユニオン型
type CounterAction = {
  type: ValueOf<typeof CounterActionType> // counter/increment | counter/decrement
}

type CounterState = { count: number }

// reducer (state, action): newState
const reducer = (state: CounterState, action: CounterAction): CounterState => {
  switch (action.type) {
    case 'counter/increment':
      return { count: state.count + 1 }
    case 'counter/decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

const Counter = (): React.ReactElement => {
  // 現在のstateとreducerを呼ぶためのdispatcherを受け取る。
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: { state.count }
      <button onClick={() => dispatch({ type: 'counter/decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'counter/increment' })}>+</button>
    </>
  )
}

root.render(
  <Counter />
)

独自フック

再利用可能な処理を切り出して定義しておくこと。
名前はuse~をつける。

やってることは関数分離だが、呼び出し元のstateやeffectを使用できる

import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

const useCount = (): Array<number | (() => void)> => {
  const [count, setCount] = useState(0)
  const increment = (): void => setCount((c) => c + 1)
  return [count, increment]
}

const Example = (): React.ReactElement => {
  const [count, increment] = useCount()

  return (
    <div>
      <p>You clicked { count as number } times</p>
      <button onClick={ increment as () => void }>
        Click me
      </button>
    </div>
  )
}
root.render(
  <>
    <Example />
    <Example />
  </>
)

React Router

公式: https://reactrouter.com/en/main

  • install
npm install react-router-dom

チュートリアルから動作を見ることができる。

  • Routerが履歴管理をしてくれることで戻る動作などに対応できる(aでなくLinkタグを使うのもそのため)
  • HooksAPIを使用することで履歴やURLパラメータなどを使用できる
  • ReactGAライブラリを使用してSPAでもGoogleAnalyticsの機能を使える
  • ReactHelmetライブラリを使用することでSPAでもHeader情報を書き換えることができる

Redux

Fluxアーキテクチャを採用した状態管理ライブラリ FluxではMVCでのmodel ⇄ view の複雑な関係性をよしとせず、単方向データフローを提供する。

  • Store: アプリケーションの状態データの集合
  • Action: イベントそのもの
  • Dispatcher: Actionに応じてStoreの更新処理を行うもの
  • View: Reactなど

reduxではここにreducer: 唯一storeのデータを変更できる純粋関数を追加する。

参考画像: https://redux.js.org/tutorials/essentials/part-1-overview-concepts#redux-application-data-flow

Viewからは状態データの変更は行わず、Actionを起動する。

  • install
npm install redux react-redux

Getting Started