今更Reactやってみた ～僕の考えた最強パネルリサイズフック編～

# 概要
こんにちは。パンダマスターと申します。
またぞろ記事を書き始めました。よろしくお願いします。
最近は趣味でReactいじってるので、その辺のことを書いていきます。
ちなみに仕事ではレガシーなコードばかりです。

# 免責事項
当記事に記載されている内容、サンプルコードを使用することによって被った、いかなる損害に対しても筆者は一切責任を負いません。当記事のサンプルコードの使用に際しては自己責任にてお願いします。
上記に同意していただける方のみ、続きをお読みください。
続きを読むことによって、上記の免責事項に同意したことになります。

# 目的
IDEのツリーとエディタなどで見られる左右に別れているパネルの間にハンドルを置き、左右にドラッグすることで全体の横幅を維持したまま比率を変えるReactフックを作ります。
'use client'を見てわかる通り、Next.js上で動作させています。
ReactDnDなど、よく使われるライブラリを使う方がいいとは思うのですが、なんか自分で作りたくなったので四角い車輪の再発明と分かっていながらもやってみました。

# 参考サイト
ウィンドウサイズを取得するフックは下記のサイトを~~丸パク~~参考にしました。

https://saunabouya.com/2022/10/29/react-hooks-window-size-dynamic/

アイコンはこちらのサイトから拾いました。SVGとJSXを選んでコピーできます。

https://heroicons.com/

# 実装

まずは本体になるuseResizeフックです。コメント書くのめんどいのでここで説明します。
引数としてハンドルになるdivのrefと、左右のパネルdivのrefと横幅を渡してあげます。
mousedown、mousemove、mouseupのイベントでそれぞれ然るべき処理が走ります。
後はコード見てください。
```tsx:useResize.tsx
import React from "react"

export function useResize(props: {
    handlerRef: React.RefObject<HTMLDivElement>,
    leftPanel: {ref: React.RefObject<HTMLDivElement>, width: number},
    rightPanel: {ref: React.RefObject<HTMLDivElement>, width: number}}) {
    let originX = 0
    let isResizing = false
    let leftPanelWidth = props.leftPanel.width
    let rightPanelWidth = props.rightPanel.width
    let isEventAdded = false
    let moveX = 0
    let leftOrigin = 0
    let rightOrigin = 0
    React.useLayoutEffect(() => {
        const onMouseDown = (e: MouseEvent) => {
            originX = e.clientX
            isResizing = true
            leftOrigin = props.leftPanel.ref.current?.clientWidth===undefined?0:props.leftPanel.ref.current?.clientWidth
            rightOrigin = props.rightPanel.ref.current?.clientWidth===undefined?0:props.rightPanel.ref.current?.clientWidth
        }
        const onMouseMove = (e: MouseEvent) => {
            if (isResizing && e.clientX > 0) {
                moveX = e.clientX - originX
                leftPanelWidth = Math.max(10, leftOrigin + moveX)
                leftPanelWidth = Math.min(rightOrigin - 10 + leftOrigin, leftPanelWidth)
                rightPanelWidth = Math.max(10, rightOrigin - moveX)
                rightPanelWidth = Math.min(leftOrigin - 10 + rightOrigin, rightPanelWidth)
                props.leftPanel.ref.current?.setAttribute("style", "width: " + leftPanelWidth + "px")
                props.rightPanel.ref.current?.setAttribute("style", "width: " + rightPanelWidth + "px")
            }
        }
        const onMouseUp = () => {
            isResizing = false
        }
        if(props.handlerRef.current && !isEventAdded) {
            props.handlerRef.current!.addEventListener("mousedown", onMouseDown)
            window.addEventListener("mousemove", onMouseMove)
            window.addEventListener("mouseup", onMouseUp)
            isEventAdded = true
        }
    }, [])
}
```
アイコンです。横のしかなかったので90度回転しています。
自分でpath書くのしんどかったので。
```tsx:bars3.tsx
import styles from './bars3.module.css'

export default function Bars3Icon() {
    return (
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.0} stroke="currentColor" className={`${styles.bars3} mt-[calc((100vh-75px)/2)]`}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
        </svg>
    )
}
```
```css:bars3.module.css
.bars3 {
    transform: scale(0.8);
    transform: rotate(90deg);
}
```


そして、呼び出し側の実装です。
呼んでるでだけです。
ウィンドウサイズをベースに横幅を決めてるので、リサイズされたときは再計算しています。

```tsx:page.tsx
'use client'
import React from 'react'
import Bars3Icon from './icons/bars3'
import {useWindowSize} from './hooks/useWindowSize'
import {useResize} from './hooks/useResize'

export default function Home() {
  const [windowWidth, windowHeight] = useWindowSize()
  const treepanel = React.useRef<HTMLDivElement>(null)
  const chartpanel = React.useRef<HTMLDivElement>(null)
  const handler = React.useRef<HTMLDivElement>(null)
  const previousWindwoWidth = React.useRef(windowWidth)
  
  React.useEffect(() => {
    const leftRatio = (treepanel.current!.offsetWidth / (treepanel.current!.offsetWidth + chartpanel.current!.offsetWidth))
    const rightRatio = (chartpanel.current!.offsetWidth / (treepanel.current!.offsetWidth + chartpanel.current!.offsetWidth))
    treepanel.current!.setAttribute("style", "width: " + ((windowWidth - 30) * leftRatio) + "px")
    chartpanel.current!.setAttribute("style", "width: " + ((windowWidth - 30) * rightRatio) + "px")
    previousWindwoWidth.current = windowWidth
  }, [windowWidth])
  
  useResize({
    handlerRef: handler,
    leftPanel: {ref: treepanel, width: (windowWidth-10) * 0.20},
    rightPanel: {ref: chartpanel, width: (windowWidth-10) * 0.80}
  })
  
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-2">
      <div className="z-10 max-h-full w-screen justify-center items-center font-mono text-sm lg:flex">
        <div className="flex flex-row mt-[50px]">
          <div ref={treepanel} className={`flex flex-col w-[calc((100vw-30px)*0.2)] h-[calc(100vh-75px)] bg-slate-100 border border-r-0 border-solid border-slate-300`}></div>
          <div ref={handler} className="flex flex-col w-[10px] h-[calc(100vh-75px)] cursor-ew-resize select-none">
              <Bars3Icon/>
            </div>
          <div ref={chartpanel} className="flex flex-col w-[calc((100vw-30px)*0.8)] h-[calc(100vh-75px)] bg-slate-100 border border-solid border-slate-300"></div>
        </div>
      </div>
    </main>
  )
}
```
横幅計算の細かい数値的なところはちょっと怪しいですが、これで大体動くはず。

# 感想
正直、React、特にフックはいくら書いてもうまく書けた気がしませんし、我ながらダサいコードだと思います。
齢50にも近くなると脳がポンコツになって、新しいことを吸収できなくなっていくんだなと悲しい気持ちになります。若かったとしてもダメだろとは言わないで。
もっといい実装があると思うので、コメントに書いていただくなりすると助かります。

それではまたコードを露出するときまで、バイバイのバーイ！。