Open15

実装Memo(主にReact)

chocolatchocolat

reverse() や sort() は元の配列を変更してしまうため、まず配列を複製してから変更を加える。

  const [list, setList] = useState(initialList);

  function handleClick() {
    {/* spread 構文で元の配列を複製する */}
    const nextList = [...list];

    {/* 複製した配列に対してreverse() を実行する */}
    nextList.reverse();

    setList(nextList);
  }
 {/* 🙅‍♀️ */}
  list.reverse();
  setList(list);

https://react.dev/learn/updating-arrays-in-state#making-other-changes-to-an-array

chocolatchocolat

popstate event
browser back button の押下時に処理を実行できる

useEffect(() => {
  const handlePopstate = () => {
    // 実行したい処理
  };
  window.addEventListener("popstate", handlePopstate);
  return () => {
    window.removeEventListener("popstate", handlePopstate);
  }
}, [])
chocolatchocolat

ResizeObserver

  • Web ページ上の要素のサイズの変化を監視できる
  • ビューポートのサイズ、レイアウト、またはその他の要因の変更により要素のサイズが変化するたびに監視する
  • 変化に応じてコールバック関数を実行できる
// callback => 監視対象の要素のサイズが変化するたびに呼び出される関数
const observer = new ResizeObserver(callback);
observer.observe(targetElement);

https://blog.logrocket.com/using-resizeobserver-react-responsive-designs/
https://phuoc.ng/collection/this-vs-that/resize-event-vs-resize-observer/

chocolatchocolat

useEffect clean up

In development, your Effect will call addEventListener(), then immediately removeEventListener(), and then addEventListener() again with the same handler.

addEventListener と removeEventListener の関数の参照が異なると適切に処理されない。

useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

https://react.dev/learn/synchronizing-with-effects#subscribing-to-events

chocolatchocolat

stateの更新が非同期

const errors = validate(inputValues);
setErrors(errors);
if (errors.length > 0) return;
🙅‍♀️
setErrors(validate(inputValues));
if (errors.length > 0) return;

ここで setErrors を呼んでも、次の行での error はまだ更新されていない。
そのため error の値は「前のレンダーの値」のままになっていて、意図通りの判定ができない。

chocolatchocolat

useActionState

a Hook that allows you to update state based on the result of a form action
フォームアクションの結果に基づいて状態を更新できるフック

フォーム送信時に「処理結果」と「処理中状態(pending)」をまとめて管理できる。

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

https://react.dev/reference/react/useActionState

chocolatchocolat

search 遅延検索

ユーザーの入力が速すぎてキー入力のたびにリクエストを送信
=> throttle や debounce などのスローダウンを行う(遅延検索)

ユーザーが入力を止めてから1秒後にAPI呼び出しを行うことでサーバーの負荷を軽減

loadash ライブラリ debounce の例

npm i --save lodash
import { debounce } from "lodash";

...

const handleOnChange = useCallback((e) => { console.log(e.target.value) }, []);
const debouncedHandleOnChange = debounce(handleOnChange, 100);

<input onChange={(e) => debouncedHandleOnChange(e)} />

ここでの useCallback

コンポーネントが再レンダリングされるたびに

  1. handleOnChange が新しく作られる
  2. debounce(handleOnChange, 100) も新しく作られる
  3. これまでの debounce のタイマーがリセットされる

debounce のカウントが正しく機能しなくなる可能性があるため、 useCallback でwrapすることにより同じ関数を再利用できるようになる。

chocolatchocolat

useEffect の依存配列が boolean で特定の値の場合のみ処理をしたい場合

  useEffect(() => {
    if (isEdit === false) {
      something();
    }
  }, [isEdit]);

true -> false の場合にのみ再レンダリングされれば良い
false -> false true -> true のときにもレンダリングされてしまう

const prevIsEdit = useRef<boolean>(isEdit);

  useEffect(() => {
    if (prevIsEdit.current === true && isEdit === false) {
      something();
    }
    prevIsEdit.current = isEdit;
  }, [isEdit]);
chocolatchocolat

debounce をコンポーネントの最上位で使用しない

問題点

  • レンダリングの度にタイマーがリセットされる
  • クリーンナップされない(直前のタイマーが再レンダリングされた際に残る)
  • メモリーリーク
  const debounceHandleOnChange = useMemo(() => {
    return debounce(handleOnChange, 300);
  }, [handleOnChange]);

  // clean up debounce
  useEffect(() => {
    return () => {
      debounceHandleOnChange.cancel();
    };
  }, [debounceHandleOnChange]);

<input onChange={(e) => debounceHandleOnChange(e.target.value)} />
chocolatchocolat

custom file upload button

const fileInputRef = useRef(null);

const handleClick = event => {
  fileInputRef.current.click();
};

const handleChange = (event) => {
  const fileUploaded = event.target.files[0];
  handleFile(fileUploaded);
};

return (
  <>
      <button onClick={handleClick}>
        Upload a file
      </button>
      <input
        type="file"
        onChange={handleChange}
        ref={fileInputRef}
        style={{display: 'none'}} // Make the file input element invisible
      />
  </>
);

https://medium.com/web-dev-survey-from-kyoto/how-to-customize-the-file-upload-button-in-react-b3866a5973d8

chocolatchocolat

file upload button with size limit

const handleOnChange = (event) => {
  if (!event.target.files) return false;
  const file = e.target.files[0];

  // max 2MB
  if (file.size > 2 * 1000 * 1024) {
    // error handling
    e.target.value = "";
    return false;
  }
  // do something with file
};

<input
  type="file"
  accept="image/*"
  onChange={handleOnChange}
/>
chocolatchocolat

SSR と SSG

SSG(Static Site Generation) 静的
ビルド時にHTMLを生成(サーバーでHTMLに変換されてブラウザに届く)

npm run build実行時:
page.html を生成
→ Cloudflare Pagesにアップロード
→ ユーザーアクセス時: 既に完成したHTMLを即座に返す

例)ユーザープロフィール表示ページ, ブログ記事表示ページ

SSR(Server-Side Rendering) 動的
リクエスト時にHTMLを生成(ブラウザでJavaScriptが実行される)

ユーザーアクセス時:
リクエスト受信 → データベースからデータ取得 → HTMLを動的生成 → 返す

例)ユーザープロフィール編集ページ, ブログ記事編集ページ