📡

React で Modal や Confirm の実装を簡単にする react-call というライブラリがアツい!!!

2025/01/21に公開
2

タイトルの通り、めちゃくちゃ良さげなライブラリ react-call を見つけたので紹介するコーナー
https://github.com/desko27/react-call

実際の動きはわかりやすいデモページがあるので見てください👍
https://react-call.desko.dev/

react-call とは

react-call がもたらす効果は「ReactComponent を手続き的に処理できるようにする」というのが私の理解です。
これが何を意味するのかというと、Modal や Confirm のような「別のコンポーネントから任意のタイミングで呼び出したい(≒表示したい)」また「その結果(≒値など)を受け取りたい」というごく一般的な要件をシンプルに解決します🙌

詳しく見ていきましょう!

window.confirm との比較

下記は README にある例です。

window.confirm
const message = 'Sure?'
const yes = window.confirm(message)

if (yes) thanosSnap() // 🫰
react-call
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 を追加します

App.tsx
+ <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 を自作するにしても、 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 への依存は useStateuseEffect だけと言うのがわかります。

https://github.com/desko27/react-call/blob/v1.4.0/lib/createCallable/index.tsx

react-dom や WebAPI 等への依存は無いので、シンプルに ReactNative でも動作するということですね🎉

軽量で依存なし

React の標準機能だけで作成されている薄いライブラリのため、非常に軽量でかつ React 以外に依存しているライブラリがありません。

もし依存が多岐にわたる場合は、依存先のセキュリティリスクなどでアップデートの必要に迫られたり、依存先のライブラリが死んでしまうことによって一緒に死んでしまうリスクがあります。
それ以外にも React 新機能の対応に遅れることもしばしばありますね...

react-call はそういったリスクがほとんどないので、導入するのにも参考にするのにも障壁が低くてGoodなポイントですね👍
(まだリリースされたばかりで、かつ個人開発みたいなので継続してメンテナンスされるかは今後の動向次第です)


余談: 今後の期待 - 内容が古くなったので閉じています

余談: 今後の期待

残念ながらこの記事を嬉々として書いている 2025.1 現在では、react-call で呼び出したコンポーネントをプログラマブルに終了させる機能は存在していません。

Confirm.call(props); // ← 外から呼び出せても
Confirm.end()        // ← 外から閉じる機能はない

これは下記 issue で検討されているようです

https://github.com/desko27/react-call/issues/26

というのも、useEffect のタイミングやその他要件によってプログラマブルに Modal 等を閉じたいというケースがすくなからず存在するからです。

useEffect に関しては window.alert などキャンセルすることができない機構で、1度だけ呼びたいが実際は2回呼ばれてしまうという StrictMode の挙動 と相性の悪い問題があります。

これに対する React 公式の回答では「キャンセルできる機構を設けろ」なので、react-call がこれに対応できると明確な差別要因になると考えています。
(もちろんそれがなくても、標準 API よりデザインのカスタム幅があり、この機構を自作する必要がない/設計を参考にできるなど強力なメリットはすでに存在していますが)

外部から終了させる機構: 2025.1.23 追記

v1.5.0 のリリースにより、外部から終了させることが可能になりました!🎉
下記例のように、promiseend メソッドに渡すことで終了させることができます。

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 が良さそうだと思った方は今すぐスターしに行きましょう!🌟

https://github.com/desko27/react-call

Discussion

desko27desko27

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! 💪

コメントありがとうございました👍