Open11

いまさら React に入門する

sterashima78sterashima78

プロジェクトのセットアップ

$ npx create-react-app test-app --template typescript --use-npm
$ code test-app

スクリプト

# 開発サーバ
$ npm start 

# テスト
$ npm test # インタラクティブモード

# ビルド
$ npm run build

eject は設定ファイルを出力するためのもの。
(これを npm script に入れておくのは微妙じゃないか?)

sterashima78sterashima78

コンポーネント

ステートレス

import React from "react";

// 静的なコンポーネント
export const Test1 = () => <span>test1</span>

// props を使うコンポーネント
export const Test2: React.VFC<{msg: string, children: ReactNode}> = ({ msg, children }) => (
    <div>
        <p>{msg}</p>
        {children}
    </div>
)

children は tsx でタグネストをした子要素のノードが入る。

型付けがちゃんと効くので警告が出る。

ステートフル

useState

// ローカルステート・イベントハンドラ
export const Test3 = () => {
    const [count, setCount] = useState(0)
    return <div>
        <p>Count: {count}</p>
        <button onClick={ ()=> setCount(count + 1) }>increment</button>
    </div>
}

useReducer

export const Test4 = () => {
    const [count, increment] = useReducer((a)=> a + 1,0)
    return <div>
        <p>Count: {count}</p>
        <button onClick={ increment }>increment</button>
    </div>
}
sterashima78sterashima78

ステートから算出する値

Vue.js のcomputed 感がある。
count2 は依存している値を明示していないので再算出されない

基本的に再評価されるときに関数内は再評価されるらしいから、
明確に依存する値が変更したときだけしか計算してほしくないときに使う感じっぽい。
デメリットないなら、演算が必要なものは毎回こう書いてもいい気がするけど。

export const Test5 = () => {
    const [count, increment] = useReducer((a) => a + 1, 0)
    const count1 = useMemo(()=> count * 2, [count])
    const count2 = useMemo(()=> count * 2, [])
    return <div>
        <p>Count: {count}</p>
        <p>Count1: {count1}</p>
        <p>Count2: {count2}</p>
        <button onClick={ increment }>increment</button>
    </div>
}

sterashima78sterashima78

ライフサイクルフック

export const Test6: React.VFC<{ count: number }> = ({ count }) => {
    useLayoutEffect(()=> console.log("Test6: before render"))
    useEffect(()=> console.log("Test6: after render"))

    return <p>Count: {count}</p>
}

export const Test7: React.VFC<{ onClick: ()=> void }> = (props) => {
    useLayoutEffect(()=> console.log("Test7: before render"))
    useEffect(()=> console.log("Test7: after render"))

    return <button { ...props }>increment</button>

}

export const Test8 = () => {
    const [count, increment] = useReducer((a: number) => a + 1, 0)

    useLayoutEffect(()=> console.log("Test8: before render"))
    useEffect(() => console.log("Test8: after render"))
      
    return <>
        <Test6 count={count} />
        <Test7 onClick={ increment } />
    </>
}

<></> はフラグメント。ルートエレメントを複数にしたいときに使う。

useLayoutEffect はコンポーネントレンダリング前に発火。
useEffect はコンポーネントレンダリングあとに発火

sterashima78sterashima78

メモ化

increment がコールされると、count カウントが更新される。
そうすると、 Test8 が再レンダリングされる。
親コンポーネントが再レンダリングされるときに、子コンポーネントの Test7, Test6 も再レンダリングされる。

一方で Test7Test6 は純粋関数なので、パラメータが変化しない限りレンダリング結果が同じになる。
したがって、再レンダリングが行われる必要がない。
こういうときは memo 関数を使うとパラメータが変わらない限りコンポーネントの再レンダリング行われない。

Test7 をメモ化しておく。

export const Test7: React.VFC<{ onClick: ()=> void }> = memo((props) => {
    useLayoutEffect(()=> console.log("Test7: before render"))
    useEffect(()=> console.log("Test7: after render"))

    return <button { ...props }>increment</button>
})

Test7 が再レンダリングされない。
Test8 が再レンダリングされるときに increment が再生成されて、useCallBack を使って... という感じになると思ったがそうではないらしい。それならよし。

Test8 にローカルステートを増やす

export const Test8 = () => {
    const [count, increment] = useReducer((a: number) => a + 1, 0)
    const [value, setValue] = useState("")

    useLayoutEffect(()=> console.log("Test8: before render"))
    useEffect(() => console.log("Test8: after render"))
    
    return <>
        <p>Value: {value}</p>
        <input onInput={(e)=> setValue(e.currentTarget.value) } />
        <Test6 count={count} />
        <Test7 onClick={ increment } />
    </>
}

Test6 が依存している count が変化していないのに Test6 が再レンダリングされている。
Test6 を メモ化する。

export const Test6: React.VFC<{ count: number }> = memo(({ count }) => {
    useLayoutEffect(()=> console.log("Test6: before render"))
    useEffect(()=> console.log("Test6: after render"))

    return <p>Count: {count}</p>
})

count が変化したときだけ Test6 が再レンダリングされる。

sterashima78sterashima78

Context

Vue.js の Provide / Inject と同じような使い方するやつ

useCounter.tsx
import React, { createContext, useContext, useState } from "react";

const CounterContext = createContext<{
      count: Readonly<number>;
      increment: () => void;
      decrement: () => void;
    }
  >({
    count: 0,
    increment: ()=> null,
    decrement: ()=> null,
  });


const increment = (count: number, updateCount: (v: number) => void) => () =>
  updateCount(count + 1);
const decrement = (count: number, updateCount: (v: number) => void) => () =>
  updateCount(count - 1);

export const CounterProvider: React.FC = ({ children }) => {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider
      value={{
        count: count,
        increment: increment(count, setCount),
        decrement: decrement(count, setCount),
      }}
    >
      {children}
    </CounterContext.Provider>
  );
};
export const useCounter = () => useContext(CounterContext);
components.tsx
export const Test9 = () => {
  const { count } = useCounter();
  return <p> count * 2 = {count * 2} </p>;
};

export const Test10 = () => {
  const { increment, decrement } = useCounter();
  return (
    <>
      <p>
        <button onClick={increment}>increment</button>{" "}
      </p>
      <p>
        <button onClick={decrement}>decrement</button>{" "}
      </p>
    </>
  );
};
App.tsx
export const App = ()=> <CounterProvider>
        <Test9 />
        <Test10 />
</CounterProvider>