Open11

いまさら React に入門する

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

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

スクリプト

# 開発サーバ
$ npm start 

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

# ビルド
$ npm run build

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

コンポーネント

ステートレス

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>
}

ステートから算出する値

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>
}

ライフサイクルフック

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 はコンポーネントレンダリングあとに発火

メモ化

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 が再レンダリングされる。

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>

ルーティング / SSR

どうせ Next.js 使うから別にいい。

スタイル

これもNext.js 使うなら設定されてるっぽいからいいか。

ログインするとコメントできます