Open7

ReactとかTypescriptでわかったことを小さなことから淡々と

ぺんぺんぺんぺん

今やってること

「TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発」という本を見ながらTypescriptとかReactを勉強中。

※以降のスクラップは以下の書籍を参考にしています。

ぺんぺんぺんぺん

Reactでイベントハンドリングした時、イベントの関連オブジェクトを取得する方法

const Name = () => {
  // React.ChangeEventはonChangeイベントが発生したときの関連オブジェクトを取得することができる。
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value + "と入力しましたね?");
  };
  return <input onChange={onChange} type="text"></input>;
};

export default Name;

onChangeメソッドの引数で値を渡すのかと思っていたけど、そんな必要はなかった。

ぺんぺんぺんぺん

reactコンポーネントの引数の話

以下の書き方だとOKで、

const Text = (props: { content: string }) => {
  return <p className="text">{content}</p>;
};
const Message = () => {
  return <Text content="test" />;
};

export default Message;

以下の書き方だとエラーが出る、

const Text = (content:string) => {
  return <p className="text">{content}</p>;
};
const Message = () => {
  return <Text content="test" />;
};

export default Message;
型 '{ content: string; }' を型 'string' に割り当てることはできません。

調べた感じだと基本的にオブジェクト型で受け取るっぽい。
(もしかしたら普通に文字列として渡す方法とかもあるかもしれない?)

なのでこれでもOK(全体を統一するならこの書き方にしたほうがいいのかもしれない)

type TextType = {
  content: string;
};
const Text = (props: TextType) => {
  const { content } = props;
  return <p className="text">{content}</p>;
};
const Message = () => {
  return <Text content="test" />;
};

export default Message;
ぺんぺんぺんぺん

分割代入の話

type TextType = {
  content: string;
};
const Text = (props: TextType) => {
  const { content } = props;
  return <p className="text">{content}</p>;
};

上記のコードでpropsに受け取った値を{content}という形で受け取っている。
これは分割代入と呼ばれるもので、propsの型に宣言されている変数名と同一名称の変数に値を代入する機能。
なので、以下のようにTextTypeに存在しない名称の変数に値を代入しようとするとエラーが出る。

type TextType = {
  content: string;
};
const Text = (props: TextType) => {
  const { aaa } = props;
  return <p className="text">{aaa}</p>;
};
プロパティ 'aaa' は型 'TextType' に存在しません。
ぺんぺんぺんぺん

ContextAPI(ConsumerとProvider)

コンポーネント作成では親要素から子要素に値を渡すが、子の子要素に渡したいとなると、以下のようにバケツリレーで渡すことになる。

const Children2 = (props: { str: string }) => {
  return <h1>{props.str}</h1>;
};

const Children1 = (props: { str: string }) => {
  return (
    <div>
      <Children2 str={props.str} />
    </div>
  );
};

const Parent = () => {
  const str: string = "test";
  return (
    <Children1 str={str} />
  );
};

export default Parent;

それを解消する仕組みがContextAPIと呼ばれるもの。
以下のコードのように、Contextを作成した後、Consumerに値を設定すると、Providerで取得することができる。

import React from "react";
// ①createContextでContextを作成する
const TestContext = React.createContext("");

const Children2 = () => {
  return (
    // ③Consumerで囲み、Providerで
    <TestContext.Consumer>
      {(str) => {
        return <h1>{str}</h1>;
      }}
    </TestContext.Consumer>
  );
};

const Children1 = () => {
  return (
    <div>
      // 子の子要素には何も値を渡していない。
      <Children2 />
    </div>
  );
};

const Parent = () => {
  const str: string = "test";
  return (
    // ②Providerに値を設定し、子要素を囲む
    <TestContext.Provider value={str}>
      <Children1 />
    </TestContext.Provider>
  );
};

export default Parent ;

また同一のContextのProviderで囲んでいる場合、Consumerで取得できる値は、Consumerから見て最も近いProviderで設定された値となる。

import React from "react";
const TestContext = React.createContext("");

const Children2 = () => {
  return (
    <TestContext.Consumer>
      {(str) => {
       // 表示される値はSecondContext
        return <h1>{str}</h1>;
      }}
    </TestContext.Consumer>
  );
};

const Children1 = () => {
  return (
    <div>
      <Children2 />
    </div>
  );
};

const Parent = () => {
  const str1: string = "FirstContext";
  const str2: string = "SecondContext";
  return (
    <TestContext.Provider value={str1}>
      <TestContext.Provider value={str2}>
        <Children1 />
      </TestContext.Provider>
    </TestContext.Provider>
  );
};

export default Parent ;
ぺんぺんぺんぺん

useState関数の引数で受け取る値は直前の値

ちょっと戸惑ったのでメモ。
以下のコードのようにsetCountにprevCountという値を渡している。
prevCountには直前の状態の値が保持されているが、パッと見prevという接頭辞がついてることでそういうふうにプログラム側で解釈されると思っていた。

  // 初期値として値を設定
  const [count, setCount] = useState(initialValue);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>1増やすよ</button>
    </div>
  );

全然関係なくて以下の書き方でもOK。

  // 初期値として値を設定
  const [count, setCount] = useState(initialValue);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((count) => count + 1)}>1増やすよ</button>
    </div>
  );

何なら適当な変数名でもOK。
普通に考えたらJavascriptのアロー関数の引数宣言してるだけなのだから関係ないって気づけましたよね…orz

ぺんぺんぺんぺん

strictモードでハマった話

index.tsx、Test.tsxをそれぞれ以下のように定義。

index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import Test from "components/Test";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <div>
      <Test />
    </div>
  </React.StrictMode>
);

reportWebVitals();
Test.tsx
import { useState } from "react";

const Test = () => {
  const [count, setCount] = useState(1);
  console.log(`Parentが描画されました,count=${count}`);

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <p>{`現在のカウント:${count}`}</p>
    </div>
  );
};

export default Test;

この状態で「+1」ボタンを押下すると、コンソールログが2回表示されてしまった。
初心者なので、なにか実装間違ってるのかと思ってたけど、原因は「React.StrictMode」にありました。

というのも・・・

React.StrictModeはアプリケーションの潜在的な問題点を洗い出すためのツールで、色々チェックしてくれています。
※詳しくはReact API リファレンス <StrictMode>
strictモードが機能しているとレンダー、およびエフェクトが1回追加で実行されるそうです。(ちなみにReact18からの挙動だそうです)
つまり、ボタンを1回押したけど、strictモードの機能で1回追加実行されるため、2回コンソールログが出たということになります。

どうしたらいいの・・・?

strictモードが機能するのは開発中だけなので、特段気にしなくても良いと思います。
ただ本当にバグっているせいで出ているのでは?と気になる場合はindex.tsxからReact.StrictModeのタグをコメントアウトすれば機能しなくなります。

まとめ

  • React.StrictModeはアプリケーションの潜在的な問題を洗い出すためのツール。
  • React.StrictModeはレンダーやエフェクトを1回追加実行する。
  • 機能を止めたい場合はReact.StrictModeタグをコメントアウトする。