aono1234
@aono1234

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

React+TypeScript useRefでエラーが発生してしまう

React+TypeScript useRefでエラーが発生してしまう

Reactで書いたコードにTypeScriptを実装しようとしているのですが
Workspace.tsxファイルの
technologyRefs.current[i] = React.createRef()
という箇所でtypescriptのエラーが発生してしまいます。

Type 'RefObject<unknown>' is missing 
the following properties from type 'HTMLDivElement': align, addEventListener,
removeEventListener, accessKey, and 284 more.
Workspace.tsx
import './Workspace.css'
import React, { useEffect, useState, useRef, MutableRefObject } from "react"
import Technology from "./Technology";
import Arrow from "./Arrow";
import {useArrow, ArrowPosRect} from "../hooks/useArrow";
import useComponentArray from "../hooks/useComponentArray";
import useTechnology from '../hooks/useTechnology';

const Workspace = (props: workspaceProps) => {
  const technologyRefs: {current: HTMLDivElement[]} = useRef<HTMLDivElement[]>([])
  const [topTechnologyId, setTopTechnologyId] = useState("")
  const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([])
  const [arrows, setArrows] = useState([])
  const [
    liftUpTechnology,
    liftDown,
    selectTechnology
  ] = useTechnology(topTechnologyId, setTopTechnologyId, selectedNodeIds, setSelectedNodeIds)
  const [getArrowProperties]: [(startElementId: string, endElementId: string) => ArrowPosRect] = useArrow(technologyRefs)
  const arrowHandler = () => {
    selectedNodeIds.forEach((selectedNodeId) => {
      const arrowProperties = getArrowProperties(topTechnologyId, selectedNodeId)
      setArrows(arrows => {return(
        [
          ...arrows, 
          <Arrow
            width={arrowProperties.rect.width}
            height={arrowProperties.rect.height}
            startPosition={[arrowProperties.positions.startPosition.x, arrowProperties.positions.startPosition.y]}
          />
        ]
      )})
    })
  }

  const resetTechnologies = () => {
    setSelectedNodeIds([])
    setTopTechnologyId(null)
  }

  return(
    <div className="workspace" onMouseDown={resetTechnologies}>
      {arrows}
      <button onMouseDown={(e) =>{
        arrowHandler()
        e.stopPropagation()
      }
      }>矢印描画</button>
        {props.technologies.map((technology, i) => {
          technologyRefs.current[i]  = React.createRef() //ここでエラー発生
          return(
            <div draggable
              onMouseDown={(e) => {
                selectTechnology(e)
                e.stopPropagation()
              }}
              onDragStart={liftUpTechnology}
              onDragEnd={liftDown}
            >
              <Technology
                name={technology.name}
                key={technology.id}
                id={technology.id}
                ref={technologyRefs.current[i]}
                selectedNodeIds={selectedNodeIds}
                topTechnologyId={topTechnologyId}
              />
            </div>
          )
        })}
    </div>
  )
}

export default Workspace

technologyRefsの中は下画像のような感じになっています。
image.png

Technologyコンポーネントは以下のように実装しています。

Technology.tsx
import React, { useEffect, forwardRef } from "react"

type TechnologyProps = {
  id: string;
  name: string;
  topTechnologyId: string | null;
  selectedNodeIds: string[];
};

const Technology = forwardRef<HTMLDivElement, TechnologyProps>((props: TechnologyProps, technologyRef: React.RefObject<HTMLDivElement>) => {
  useEffect(() => {
    if (technologyRef.current) {
      if (props.selectedNodeIds.includes(props.id.toString())) {
        technologyRef.current.style.border = 'dashed'
      } else {
        technologyRef.current.style.border = 'solid'
      }
      if (props.topTechnologyId == props.id) {
        technologyRef.current.style.borderColor  = "red"
      }
    }
  }, [props.topTechnologyId, props.selectedNodeIds, technologyRef])

  return (
    <>
      <div ref={technologyRef}
        className='technology'
        id={props.id}
        style={{ cursor: "pointer" }}
      >
        {props.name}
      </div>
    </>
  )
})

export default Technology

technologyRefsの中に配列で要素を入れているからエラーが発生するのでしょうか…?

すみませんが、ご教授頂けますとありがたいです。
以上、よろしくお願い致します。

0

2Answer

HTMLElementへのref参照は ref属性を適切に指定することによって取得してください.
ref属性にコールバック関数を指定すると,その引数にrefに保持すべきオブジェクトが渡されます.これを格納すればよいです.

const arrayRef = useRef<HTMLElement[]>([])

// 都合上nodeはnullableなので要チェック
<div ref={node => if(node) arrayRef.current[i] = node}>...
0Like

Comments

  1. technologyRefs.current[i] = React.createRef()はRefオブジェクトの中にRefオブジェクトを格納するという意味であり通常無駄な行為です.
    何らかの都合があって実ノードを参照しないといけないという時にtechnologyRefs.current[i].currentなんてことする羽目になりますので…

以下のように、配列的に表示されるアイテム(ここでは Technology)の取りうる状態については、 Props として定義し、親から流し込むのが定石です。

このようにすれば ref も必要ありませんし、「子(Technology)は親が何をしているのか必要以上に知らない」という理想的なモジュールの分割が実現できます。

(他の質問と重複してしまいますが、)
スタイルは、 ref を用いて DOM を直に操作するよりも、 JSX の式の中で style Props を指定したほうが好ましいです。 view = f(props, state); の原則に従っているので、意図が明確になります。

Technology.tsx
type Props = {
  name: string,
  selected: boolean,
  moving: boolean, // 移動中の状態? topTechnologyId の意味を推測して付けました
}

const Technology = (props: Props) => {
  return (
    <div
      className='technology'
      style={{
        cursor: "pointer",
        borderStyle: props.selected ? "dashed" : "solid",
        ...(props.moving ? { borderColor: "red" } : {}),
      }}
    >
     {props.name}
    </div>
  );
}
Workspace.tsx
<Technology
  key={technology.id}
  name={technology.name}
  selected={selectedNodeIds.includes(technology.id)}
  moving={topTechnologyId === technology.id}
/>
0Like

Your answer might help someone💌