◆ hook にまとめる
◆ Promise を返す関数を用意して閉じられたときに結果を受け取れる

前に React でダイアログを扱うときに hook を使って open 状態を管理しなくていいようにしましたが ダイアログに限らず表示の ON/OFF を切り替える系のコンポーネント全体で楽に扱えるようにしたいなと思って何か作ってみることにしました

基本的に props で open と onClose を取るものです
例として Dialog コンポーネントを手抜きで作りました

const Dialog = ({ children, open, onClose }) => {
return (
<div hidden={!open}>
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "#0004",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
onClick={(event) => event.target === event.currentTarget && onClose()}
>
<div
style={{
width: "30vw",
height: "20vw",
backgroundColor: "white",
padding: "10px 20px",
}}
>
{children}
</div>
</div>
</div>
)
}

こんな感じで開いたり閉じたりするコンポーネントが対象です
これを普通に使う場合はこんな感じです

const Component1 = () => {
const [open, setOpen] = useState(0)
const [result, setResult] = useState(null)

const onClose = (value) => {
setOpen(0)
setResult(`${open}-${value}`)
}

return (
<div>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={() => setOpen(1)}>open1</button>
<button onClick={() => setOpen(2)}>open2</button>
</div>
<p>result is {result}</p>
<Dialog open={open} onClose={() => onClose(0)}>
<p>
outside: 0<br />
button1: 1<br />
button2: 2
</p>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={() => onClose(1)}>1</button>
<button onClick={() => onClose(2)}>2</button>
</div>
</Dialog>
</div>
)
}

開いた方法と閉じた方法によって閉じたときになにかの処理をします
どのボタンで開いたかは open 状態を管理する state とひとつにまとめて true/false ではなく数値にしてます
閉じられたときに結果を 1-1 や 2-0 みたいな形で result に入れて表示しています
「{開いた方法}-{閉じた方法}」 としてます

これくらいならこのままでもいいですが もっと複雑になってくるとわかりづらくなってきます
特にこの方法だと開くときと閉じるときがわかれます
標準の confirm 関数だと開く関数の返り値として結果を得られて便利ですよね
そういう感じにします

表示状態を管理するための hook で useOpen というのを作りました

const useOpen = () => {
const [resolve, setResolve] = useState(null)

const open = () => {
return new Promise(resolve => {
setResolve(() => resolve)
})
}

const render = (renderFn) => {
const onClose = (value) => {
setResolve(null)
resolve(value)
}
return renderFn({ open: !!resolve, onClose })
}

return { open, render }
}

ダイアログに固定しないのでこの中で特定コンポーネントは使わず render 関数を返してここで使う側が好きなコンポーネントを使えるようにしてます
使う例はこんなのです

const Component2 = () => {
const { open, render } = useOpen()
const [result, setResult] = useState(null)

const open1 = async () => {
const result = await open()
setResult(`1-${result}`)
}

const open2 = async () => {
const result = await open()
setResult(`2-${result}`)
}

return (
<div>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={open1}>open1</button>
<button onClick={open2}>open2</button>
</div>
<p>result is {result}</p>
{render(({ open, onClose }) => (
<Dialog open={open} onClose={() => onClose(0)}>
<p>
outside: 0<br />
button1: 1<br />
button2: 2
</p>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={() => onClose(1)}>1</button>
<button onClick={() => onClose(2)}>2</button>
</div>
</Dialog>
))}
</div>
)
}

open1, open2 関数みたいに await を使って閉じたときに結果の値を受け取れます
render 関数の中に Dialog を書くので JSX 内にまとまって自然な感じです
render 関数に渡す関数は open, onClose を受け取るのでこれを使用します

ただ開き方によって中身を変えたいこともありそうなので open の引数で render 関数を渡すのでも良いかもしれないです
ただそうすると JSX の部分がまとまらないので open に渡した引数を render 関数で open や onClose と同じように受け取れるようにして 関数内で分岐も良さそうです

JSX の場所をまとめるためだけに render 関数を用意してますが コンポーネントを分けてしまう前提で render 関数ではなく element を受け取るようにもできます
Dialog コンポーネントは中になんでも表示できる汎用的なものでしたが 今回の中身を表示する専用のコンポーネントを作って Dialog1 とします

const Dialog1 = ({ open, onClose }) => {
return (
<Dialog open={open} onClose={() => onClose(0)}>
<p>
outside: 0<br />
button1: 1<br />
button2: 2
</p>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={() => onClose(1)}>1</button>
<button onClick={() => onClose(2)}>2</button>
</div>
</Dialog>
)
}

これを useOpen の引数にできるようにします

const useOpen = (Component) => {
const [resolve, setResolve] = useState(null)

const open = () => {
return new Promise(resolve => {
setResolve(() => resolve)
})
}

const onClose = (value) => {
setResolve(null)
resolve(value)
}

const element = <Component open={!!resolve} onClose={onClose} />

return { open, element }
}

const Component3 = () => {
const { open, element } = useOpen(Dialog1)
const [result, setResult] = useState(null)

const open1 = async () => {
const result = await open()
setResult(`1-${result}`)
}

const open2 = async () => {
const result = await open()
setResult(`2-${result}`)
}

return (
<div>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={open1}>open1</button>
<button onClick={open2}>open2</button>
</div>
<p>result is {result}</p>
{element}
</div>
)
}

シンプルでスッキリしましたが渡せる props が制限されるのが場合によっては不便かもしれません
渡せるようにする場合 開き方で変わらないなら useOpen の第二引数で渡して 開き方で変わる場合は open の引数でしょうか

色々やりようがあってベストがこれっていうのはないですけど 普通に open と onClose を自分で管理するよりは扱いやすくなりました
コンポーネント内に open するものがいくつもある場合は管理が面倒でしたからね