React で Modal や Confirm の実装を簡単にする react-call というライブラリがアツい!!!
タイトルの通り、めちゃくちゃ良さげなライブラリ react-call
を見つけたので紹介するコーナー
実際の動きはわかりやすいデモページがあるので見てください👍
react-call
とは
react-call
がもたらす効果は「ReactComponent を手続き的に処理できるようにする」というのが私の理解です。
これが何を意味するのかというと、Modal や Confirm のような「別のコンポーネントから任意のタイミングで呼び出したい(≒表示したい)」また「その結果(≒値など)を受け取りたい」というごく一般的な要件をシンプルに解決します🙌
詳しく見ていきましょう!
window.confirm
との比較
下記は README にある例です。
const message = 'Sure?'
const yes = window.confirm(message)
if (yes) thanosSnap() // 🫰
const props = { message: 'Sure?' }
const yes = await Confirm.call(props)
if (yes) thanosSnap() // 🫰
Web 標準である window.confirm
の インターフェースとほぼ同じで、Promise
を利用して返却値の解決を待っているのが主な違いですね。
Promise
の場合、他の処理をブロックすることなく結果を待機することかできるので、こちらの方が何かと都合がいいです🥰
react-call
の使い方
先の例で行っていた await Confirm.call(props)
ができるコンポーネントをどうやって作成していくかを見ていきましょう!
なおコード例は README から引用しています。
1. 呼び出したいコンポーネントを定義する
import { createCallable } from 'react-call'
interface Props { message: string }
type Response = boolean
export const Confirm = createCallable<Props, Response>(({ call, message }) => (
<div role="dialog">
<p>{message}</p>
<button onClick={() => call.end(true)}>Yes</button>
<button onClick={() => call.end(false)}>No</button>
</div>
))
createCallable
という関数が react-call
から提供されているので、それを利用して Wrap するだけです!簡単ですね!
(昔HOCパターンを利用していた方は懐かしいかもw)
注意点は、 call
という専用の prop が注入されるため、名前の衝突を避ける必要があるくらいです。
それ以外に内部的な影響はありません。
そしてこの call
オブジェクトには end
というメソッドが生えており、上記例のように call.end(response)
とすることで呼び出し側に結果を返すことができます。
2. 定義したコンポーネントを Root に追加する
App.tsx
のような上位の1箇所に Root を追加します
+ <Confirm.Root />
この Root が Confirm.call(props)
のような呼び出しを listen しており、かつレンダリングを担当しているものです。
Modal などを自作したことのある方は馴染みがあるかもしれませんね。
3. 任意の場所・タイミングで呼び出す
あとは最初の例のように、Confirm
コンポーネントを使いたいところで import
して call
するだけです!
const accepted = await Confirm.call({ message: 'Continue?' })
たったこれだけで、よくあるボタンが押されたらモーダルを開いて、そのモーダル内の結果によって処理を変えるような、一般的なユースケースを簡単に解決できるようになります🎉
react-call
の良さ
react-call
がもたらす効果は「ReactComponent を手続き的に処理できるようにする」と述べました。
Modal を自作したことがある方はこの便利さが伝わったと思っているのですが、そうでない方のために実際にどういうケースで嬉しいかというのを具体的に書き連ねたいと思います。
Modal などの表示制御をしなくてよくなる
Modal を自作するにしても、 MUI のような CSSフレームワーク/コンポーネントライブラリ を利用していても、モーダルの開閉状態を useState
などで制御したことはあると思います。
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<Button onClick={handleOpen}>Open Modal</Button>
<Modal open={open} onClose={handleClose}>
<!-- 略 -->
</Modal>
</>
);
react-call
だと先のコード例で見た通り、こういった状態管理が不要になります👋
さらにネストや結果の返却などにも react-call
は対応しています。
なので後でネストできる Modal が欲しくなったり、後で Modal から返却値が欲しくなったりといった、一般的なユースケースの増加に合わせたスケール考慮も不要になります。
このネストや返却値の対応は、自作するには中々骨が折れる割にリターンが少なく虚無りがちなんですよね…
react-call
ではその分 Modal 等の実装がシンプルにでき、本質的な見た目の実装に注力できるということです👏
閉じる時のアニメーションに対応できる
React はその性質上、Modal など非表示にする際のアニメーションの実装には工夫が必要です。
(例えばCSSだけでは単純に実装できません😢)
また MUI のようなライブラリでも表示のアニメーションはできても、非表示のアニメーションがされないという事象に遭遇することがよくあります...
react-call
ではコンポーネントの破棄(unmount)時にディレイ時間を設定できるように対応されています。
これにより CSS によるアニメーションを描画できるようシンプルに解決してくれます🎉
+ const UNMOUNTING_DELAY = 500
export const Confirm = createCallable<Props, Response>(
({ call }) => (
<div
+ className={call.ended ? 'exit-animation' : '' }
/>
),
+ UNMOUNTING_DELAY
)
createCallable
の第二引数に unmount を遅らせたい時間(ミリ秒)を渡します。
コンポーネント内部では call.ended
という boolean
値が提供されているのでこれを利用し、true
の時に目的の CSS アニメーションを発火するクラスを付与します。
CSS アニメーション側ではディレイ時間内で完了するアニメーションを定義します。
.exit-animation {
opacity: 0;
transition: opacity .5s;
}
たったこれだけでクローズアニメーションを実装することができました🎉
react-call
について
その他 TypeScript サポート
もはや令和の時代において当たり前かもしれませんが、TypeScript がサポートされています。
(そもそも react-call
自体も TypeScript で作成されています)
なので Props の型はもちろん、call.end()
に渡す Response の型も解決されますし、呼び出し側にも型推論がしっかり効いています👍
const res = await Confirm.call(props)
// ^^^ ^^^^^
// 呼び出し側でも res, props それぞれに型推論が効く
SSR サポート
react-call
(のセットアップ) は SSR にも対応されているので Root への設置 を安心して利用することができます。
ただ注意が必要で、call
されるのはクライアント側を想定しているためサーバーサイドで実行するとエラーとなります。
const res = await Confirm.call(props)
// これをサーバーサイドで実行するとエラーになる
手続き的な処理として利用するインターフェースなので、当たり前っちゃ当たり前ですが、onClick
等のコールバック内で利用しましょう。
そのため結果的にクライアントコンポーネントでの利用となります。
サーバーサイドの処理結果に応じて呼び出して(≒表示して)おきたいという要件には、従来通りに props 等での表示制御で事足りますしね。
ReactNative サポート
サポートというと少し語弊がありますが、ReactNative でも動作するようです。
react-call
の実装(createCallable
)を見てみると、React への依存は useState
と useEffect
だけと言うのがわかります。
react-dom
や WebAPI 等への依存は無いので、シンプルに ReactNative でも動作するということですね🎉
軽量で依存なし
React の標準機能だけで作成されている薄いライブラリのため、非常に軽量でかつ React 以外に依存しているライブラリがありません。
もし依存が多岐にわたる場合は、依存先のセキュリティリスクなどでアップデートの必要に迫られたり、依存先のライブラリが死んでしまうことによって一緒に死んでしまうリスクがあります。
それ以外にも React 新機能の対応に遅れることもしばしばありますね...
react-call
はそういったリスクがほとんどないので、導入するのにも参考にするのにも障壁が低くてGoodなポイントですね👍
(まだリリースされたばかりで、かつ個人開発みたいなので継続してメンテナンスされるかは今後の動向次第です)
余談: 今後の期待 - 内容が古くなったので閉じています
余談: 今後の期待
残念ながらこの記事を嬉々として書いている 2025.1 現在では、react-call
で呼び出したコンポーネントをプログラマブルに終了させる機能は存在していません。
Confirm.call(props); // ← 外から呼び出せても
Confirm.end() // ← 外から閉じる機能はない
これは下記 issue で検討されているようです
というのも、useEffect
のタイミングやその他要件によってプログラマブルに Modal 等を閉じたいというケースがすくなからず存在するからです。
useEffect
に関しては window.alert
などキャンセルすることができない機構で、1度だけ呼びたいが実際は2回呼ばれてしまうという StrictMode の挙動 と相性の悪い問題があります。
これに対する React 公式の回答では「キャンセルできる機構を設けろ」なので、react-call
がこれに対応できると明確な差別要因になると考えています。
(もちろんそれがなくても、標準 API よりデザインのカスタム幅があり、この機構を自作する必要がない/設計を参考にできるなど強力なメリットはすでに存在していますが)
外部から終了させる機構: 2025.1.23 追記
v1.5.0 のリリースにより、外部から終了させることが可能になりました!🎉
下記例のように、promise
を end
メソッドに渡すことで終了させることができます。
const promise = Confirm.call({ message: 'Continue?' })
// For example, you could dismiss it on some event subscription
onImportantEvent(() => {
Confirm.end(promise, false)
})
// While still awaiting the response where needed
const accepted = await promise
基本的には何かしらの Callback の中で Confirm.call
するため、それから得られる promise
を扱うのには例の onImportantEvent
のように少し工夫が必要ですが、開始~終了を外部で完結して手続きできるのは幅が広がって良いですね!
最後まで読んでいただきありがとうございました!
react-call
が良さそうだと思った方は今すぐスターしに行きましょう!🌟
Discussion
Hi! Just wanted to drop by and say thank you for contributing to the visibility of the library 🙇🏻♂️ ところで日本語が好きです and it's a great honor to have a japanese article speak about react-call. ありがとうございました!
Hi!
I never thought this article would actually reach the author! 😳
Thank you so much for developing such an amazing library, react-call! ✨
I also saw the release of v1.5.0—it's super helpful! 🥰
There’s been a lot of buzz around it, both in this article and on Twitter, and it seems everyone in Japan is excited about the potential of react-call. 🔥
Keep up the great work maintaining it! 💪
コメントありがとうございました👍