Reactアプリケーションを高速化するための方法2(イベント編)

# 上位コンポーネントを経由せずにイベントをやりとりする

- 前々回  
[[React]useStateによるstate管理の問題点とその解決方法を模索した結果、3つの機能を分離するに至った話](https://qiita.com/SoraKumo/items/6267133940c15c89481e)
- 前回  
[Reactアプリケーションを高速化するための方法](https://qiita.com/SoraKumo/items/2163f75cf3b054c1f626)

## 今回の内容

　今回はコンポーネント間でイベントのやりとりをします
　これをReactの標準機能で実装しようとすると、コンポーネントで作成したDispatchを親で吸い上げて配り直す必要があります
　そういった処理を書くのは冗長になりやすいので避けたいところです。ということで、イベント処理を効率的に書く方法を紹介します

## 親コンポーネントの再レンダリングを省いたコンポーネント間のイベント処理

　高速に動作させることが前提なので、無駄な処理は省かなければなりません
　最大の無駄な処理は、上位コンポーネントの再レンダリングです
　イベントを必要とするコンポーネント間でのみ、やりとりを行います

## 今回使うパッケージ

[@react-libraries/use-local-event](https://www.npmjs.com/package/@react-libraries/use-local-event)

　イベントをコンポーネントグループの単位でローカル化するライブラリです
　eventハンドルを配るだけで、必要なコンポーネントのみがイベントを受け取れるようになります。

## Todoアプリケーションを作る

イベント操作の説明用にみんな大好きTodoアプリを作ります
入力フォームからイベントを一方的に飛ばす構造を、React上で低コストで作成します。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="JjWQeRr" data-user="sorakumo001-the-encoder" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sorakumo001-the-encoder/pen/JjWQeRr">
  localEvent</a> by 空雲 (<a href="https://codepen.io/sorakumo001-the-encoder">@sorakumo001-the-encoder</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### Todoデータの定義

```tsx
type Todo = {
  date: Date;
  title: string;
  description: string;
};
```

日付、タイトル、説明が入るようにします

### イベント用Actionの定義

```tsx
type TodoAction =
  | { type: 'add'; payload: Todo } //追加
  | { type: 'delete' }; //削除;
```

追加と削除が出来るようにします

### Todo表示用コンポーネント

```tsx
const TodoItem: VFC<{ todo: Todo; selected: boolean }> = ({
  todo: { date, title, description },
  selected,
}) => (
  <div
    style={{
      border: 'solid 1px',
      margin: '2px',
      padding: '2px',
      borderColor: selected ? 'red' : 'black',
    }}
  >
    <div>{date.toLocaleString()}</div>
    <div>{title}</div>
    <div>{description}</div>
  </div>
);
```

Todoを表示するだけですが、削除用の選択状態を持てるようにします

### Todoデータ管理、イベント処理コンポーネント

```tsx
const TodoList: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  //Todoデータ
  const [todoList, setTodoList] = useState<Todo[]>([]);
  const [selectIndex, setSelectIndex] = useState<number>();
  //イベント処理
  useLocalEvent(event, (action) => {
    switch (action.type) {
      case 'add':
        setTodoList((list) => [...list, action.payload]);
        break;
      case 'delete':
        if (selectIndex !== undefined) {
          setTodoList((list) => list.filter((_, index) => index !== selectIndex));
          setSelectIndex(undefined);
        }
        break;
    }
  });
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      {todoList.map((todo, index) => (
        <div key={index} onClick={() => setSelectIndex(index)}>
          <TodoItem todo={todo} selected={index === selectIndex} />
        </div>
      ))}
    </div>
  );
};
```

Todoデータのstateを管理するコンポーネントです
送られてきた削除や追加イベントの処理もここで行います

### イベント内容表示用コンポーネント

```tsx
const ActionLog: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  const [message, setMessage] = useState('');
  //イベント処理
  useLocalEvent(event, (action) => {
    switch (action.type) {
      case 'add':
        setMessage('追加を実行');
        break;
      case 'delete':
        setMessage('削除を実行');
        break;
    }
  });
  return <div>{message}</div>;
};
```

どんなイベントが送られてきたのか、確認するためのコンポーネントです
複数のコンポーネントで同時にイベントが処理できるのを確認するためのものです

### イベント送信用コンポーネント

```tsx
//操作イベント発生用コンポーネント
const TodoInput: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  //追加イベント送信
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    const form = e.currentTarget;
    const title = form._title.value;
    const description = form._description.value;
    dispatchLocalEvent(event, { type: 'add', payload: { date: new Date(), title, description } });
    form.reset();
    e.preventDefault();
  };
  //削除イベント送信
  const handleDelete = () => {
    dispatchLocalEvent(event, { type: 'delete' });
  };
  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex' }}>
      <div>
        <div>Title:</div>
        <div>Description:</div>
      </div>
      <div>
        <div>
          <input name="_title" required={true} />
        </div>
        <div>
          <input name="_description" />
        </div>
      </div>
      <button>追加</button>
      <button type="button" onClick={handleDelete}>
        削除
      </button>
    </form>
  );
};
```

Todoデータを入力して送信するコンポーネントです
削除イベントも送信します

### 親コンポーネント

```tsx
const App = () => {
  const event = useLocalEventCreate<TodoAction>();
  return (
    <>
      <TodoInput event={event} />
      <hr />
      <ActionLog event={event} />
      <hr />
      <TodoList event={event} />
    </>
  );
};
```

イベントハンドルを作成して配るだけのコンポーネントです
イベントの中身には関知しないので、Todoの作成や削除が行われても、このコンポーネントは再レンダリングが発生しません

### Todo入力時の再レンダリングの確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/ede38ce6-ed30-9db4-c4c6-5911478e7ac8.png)

色が付いているところが再レンダリングされている場所です
Appコンポーネントは除外されているのが確認出来ます

## まとめ

　今回の方法だとReact上のコンポーネント間のイベント処理がかなり簡略化できます
　イベントだけで無くstateも共有したい場合は[https://www.npmjs.com/package/@react-libraries/use-local-state](https://www.npmjs.com/package/@react-libraries/use-local-state)が使用出来ます
　コンポーネント間のイベント処理やデータ連係に困ったら使ってみてください

## おまけ

### ライブラリ部分

```tsx
import { useEffect, useRef } from 'react';

/**
 * Type for event control
 *
 * @export
 * @interface LocalEvent
 * @template T
 */
export interface LocalEvent<T> {
  callbacks: ((action: T) => void)[];
}

/**
 * Create a event
 *
 * @template T
 * @return {*}
 */
export const useLocalEventCreate = <T>() => {
  return useRef<LocalEvent<T>>({
    callbacks: [],
  }).current;
};

/**
 * Interpreting events
 *
 * @template T
 * @param {LocalEvent<T>} event
 * @param {LocalEvent<T>['callbacks'][0]} callback
 */
export const useLocalEvent = <T>(event: LocalEvent<T>, callback: LocalEvent<T>['callbacks'][0]) => {
  useEffect(() => {
    event.callbacks = [...event.callbacks, callback];
    return () => {
      event.callbacks = event.callbacks.filter((a) => a !== callback);
    };
  }, [event, callback]);
};

/**
 * Trigger an event.
 *
 * @template T
 * @param {LocalEvent<T>} event
 * @param {T} action
 */
export const dispatchLocalEvent = <T>(event: LocalEvent<T>, action: T) => {
  event.callbacks.forEach((callback) => callback(action));
};
```

### Todoアプリ全体

```tsx
import React, { useState, VFC } from 'react';
import {
  LocalEvent,
  dispatchLocalEvent,
  useLocalEventCreate,
  useLocalEvent,
} from '@react-libraries/use-local-event';

//Todo操作Action定義
type TodoAction =
  | { type: 'add'; payload: Todo } //追加
  | { type: 'delete' }; //削除;

//Todoデータタイプ
type Todo = {
  date: Date;
  title: string;
  description: string;
};

//Todoデータコンポーネント
const TodoItem: VFC<{ todo: Todo; selected: boolean }> = ({
  todo: { date, title, description },
  selected,
}) => (
  <div
    style={{
      border: 'solid 1px',
      margin: '2px',
      padding: '2px',
      borderColor: selected ? 'red' : 'black',
    }}
  >
    <div>{date.toLocaleString()}</div>
    <div>{title}</div>
    <div>{description}</div>
  </div>
);

//Todoリスト管理用コンポーネント
const TodoList: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  //Todoデータ
  const [todoList, setTodoList] = useState<Todo[]>([]);
  const [selectIndex, setSelectIndex] = useState<number>();
  //イベント処理
  useLocalEvent(event, (action) => {
    switch (action.type) {
      case 'add':
        setTodoList((list) => [...list, action.payload]);
        break;
      case 'delete':
        if (selectIndex !== undefined) {
          setTodoList((list) => list.filter((_, index) => index !== selectIndex));
          setSelectIndex(undefined);
        }
        break;
    }
  });
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      {todoList.map((todo, index) => (
        <div key={index} onClick={() => setSelectIndex(index)}>
          <TodoItem todo={todo} selected={index === selectIndex} />
        </div>
      ))}
    </div>
  );
};

//操作内容表示コンポーネント
const ActionLog: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  const [message, setMessage] = useState('');
  //イベント処理
  useLocalEvent(event, (action) => {
    switch (action.type) {
      case 'add':
        setMessage('追加を実行');
        break;
      case 'delete':
        setMessage('削除を実行');
        break;
    }
  });
  return <div>{message}</div>;
};

//操作イベント発生用コンポーネント
const TodoInput: VFC<{ event: LocalEvent<TodoAction> }> = ({ event }) => {
  //追加イベント送信
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    const form = e.currentTarget;
    const title = form._title.value;
    const description = form._description.value;
    dispatchLocalEvent(event, { type: 'add', payload: { date: new Date(), title, description } });
    form.reset();
    e.preventDefault();
  };
  //削除イベント送信
  const handleDelete = () => {
    dispatchLocalEvent(event, { type: 'delete' });
  };
  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex' }}>
      <div>
        <div>Title:</div>
        <div>Description:</div>
      </div>
      <div>
        <div>
          <input name="_title" required={true} />
        </div>
        <div>
          <input name="_description" />
        </div>
      </div>
      <button>追加</button>
      <button type="button" onClick={handleDelete}>
        削除
      </button>
    </form>
  );
};

// Parent component
const App = () => {
  const event = useLocalEventCreate<TodoAction>();
  return (
    <>
      <TodoInput event={event} />
      <hr />
      <ActionLog event={event} />
      <hr />
      <TodoList event={event} />
    </>
  );
};

export default App;
```