LoginSignup
1
1

More than 1 year has passed since last update.

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

Posted at

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

今回の内容

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

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

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

今回使うパッケージ

@react-libraries/use-local-event

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

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

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

See the Pen localEvent by 空雲 (@sorakumo001-the-encoder) on CodePen.

Todoデータの定義

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

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

イベント用Actionの定義

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

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

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を表示するだけですが、削除用の選択状態を持てるようにします

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

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

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

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

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

親コンポーネント

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

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

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

image.png

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

まとめ

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

おまけ

ライブラリ部分

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アプリ全体

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;
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1