上位コンポーネントを経由せずにイベントをやりとりする
今回の内容
今回はコンポーネント間でイベントのやりとりをします
これを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入力時の再レンダリングの確認
色が付いているところが再レンダリングされている場所です
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;