在React函数组件中,取代构造函数的最佳实践是什么?

在React的JSX生成中,存在一个困扰的问题。

这是在SPA等场景下,希望在页面切换后立即读取变量的情况。具体而言,假设我们要创建一个Todo功能的详细页面。我们从参数中获取id,然后获取与之相关联的Todo数据并显示详细页面。

使用react-router-dom6

import React,{ useState,useContext } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const EditTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  /*中略*/
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
        <table>
          <thead>
            <tr>
            <th>タイトル</th>
            <th>説明</th>
            <th>ステータス</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>{d_item.title}</td>
              <td>{d_item.description}</td>
              <td>{d_item.status}</td>
            </tr>
          </tbody>
        </table>
      }
      <button onClick={()=>navigate("/")} >戻る</button>
    </>
	)
}

export default EditTodo;

在React 16.7以前,使用类组件时,在构造函数中使用`constrcuter`来控制加载时的处理,所以并没有太大问题。但是,如果使用函数组件,则需要找到替代构造函数的方法。

顺便说一下,这是对之前发布的一篇文章收到读者(honey32先生)指出错误后的回应,所以暂时删除了(因为Qiita无法撤下文章)并提取了想要选取的部分。

选项1:使用useEffect钩子(本案例不适合)

当在Stack Overflow或Github的问题中搜索解决方法时,通常会得出使用useEffect钩子的答案。

如何使用函数组件(箭头函数语法)指定构造函数?

然而……

import React,{ useState,useContext,useEffect } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const EditTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  /*useEffectはJSX生成後に呼び出される*/
  useEffect(()=>{
    const item = context.todos.find((item)=>item.id == params.id)
    setItem(item)
  },[params.id,context])
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
        <table>
          <thead>
            <tr>
            <th>タイトル</th>
            <th>説明</th>
            <th>ステータス</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>{d_item.title}</td>
              <td>{d_item.description}</td>
              <td>{d_item.status}</td>
            </tr>
          </tbody>
        </table>
      }
      <button onClick={()=>navigate("/")} >戻る</button>
    </>
	)
}

export default EditTodo;

由于useEffect钩子函数是在DOM生成后执行的,所以在这种情况下,需要在DOM生成之前处理变量,否则d_item.id将保持未定义状态,从而显示错误。

类型错误:无法读取未定义的属性(读取’id’)

选项2:自定义组件化(不适合分工作业)

將這個問題解決的常見方法是將其自定義為組件。如果將其定義為自定義組件,則在生成JSX之前將對象定義為名為custom的變數,這樣處理本次操作也能順利進行。但由於變數處理的緣故,將其組件化會導致整個JSX被劃分為碎片,這樣後續的維護、功能擴展和與設計人員的分工都變得困難。所以對我來說,這不是一種我很想使用的方法。此外,在這種情況下,名為d_item的變數對整個JSX產生影響,所以自定義組件本身就變得冗長且難於控制。

import React,{ useState,useContext } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const DetailTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
 /*カスタムコンポーネントはJSX生成前に構築されるので、d_itemの変数が準備された状態にはなる*/
  const Custom = ()=>{
    const item = context.todos.find((item)=> item.id == params.id )
    setItem(item)
    return(
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
        <table>
          <thead>
            <tr>
            <th>タイトル</th>
            <th>説明</th>
            <th>ステータス</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>{d_item.title}</td>
              <td>{d_item.description}</td>
              <td>{d_item.status}</td>
            </tr>
          </tbody>
        </table>
    )
  }
	return (
    <>
      <h2>TODOを編集する</h2>
      <Custom />{/*このように部品を分離されると、デザイナーにとっては困った記述方法となる*/}
      <button onClick={()=>{navigate("/")} } >戻る</button>
    </>
	)
}

export default DetailTodo;

第三种方法:使用useMemo钩子(不常见的方法)

因此,我认为使用useMemo Hook来进行记忆化可能是作为您的替代方法的有效选择,而不是使用useEffect Hook进行自定义组件化。在我之前提供的链接中,也有回答者提到了这种替代方法,并且获得了一些好评,虽然评价不多。

如何使用函数组件(使用箭头函数语法)指定构造函数?

那位回答者的回答是这样的。

您可以使用下面的useMemo钩子作为函数组件的构造函数进行演示。有人建议使用useEffect,但它会在渲染后被调用。

另外,在这篇文章中似乎也介绍了一种替代方法。尝试使用React Hooks将类组件改写成无状态功能组件。

useMemoフックは、本来は性能を向上させるために時間やメモリを必要とする処理に対して、特定の変数が変更された時に実行されるものです。しかし、このuseMemoフックはJSX生成前に処理されるため、JSX生成時には既に変数が代入されていますので、上記のエラーを回避することができます。

import React,{ useState,useContext,useMemo } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const DetailTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  /*useMemoはJSX生成前に呼び出されるので、setItemによる変数代入が間に合う*/
  useMemo(()=>{
    const item = context.todos.find((item)=>item.id == params.id)
    setItem(item)
  },[params.id,context])
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
        <table>
          <thead>
            <tr>
            <th>タイトル</th>
            <th>説明</th>
            <th>ステータス</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>{d_item.title}</td>
              <td>{d_item.description}</td>
              <td>{d_item.status}</td>
            </tr>
          </tbody>
        </table>
      }
      <button onClick={()=>navigate("/")} >戻る</button>
    </>
	)
}

export default DetailTodo;

不过,即使在查阅了React官方手册后,也没有找到这种方法。因此,这不能被视为一种常见的解决方法,我想知道是否存在更合适的方法。顺便说一下,即使使用React.memo进行记忆化,d_item不会再出现未定义的错误,但是由于没有通过setItem方法进行变量赋值,它将为null(已验证)。

参考文章:
useMemo、useCallback和React.memo是什么?它们之间有何区别?

第四种方法:使用useCallback钩子(带有条件)

在进一步的调查中,我还发现了使用useCallback挂钩的例子,并且使用这个例子成功地展示了详细页面。我认为这更接近React的原始使用方式。需要注意的是,如果不使用useCallback挂钩时调用useState挂钩会导致无限循环错误。

顺便提一下,以下的描述仍然是一个临时答案。

import React,{ useState,useContext,useCallback } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const EditTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  const getCallback = useCallback( ()=>{
    const item = context.todos.find((item)=>item.id == params.id)
    setItem(item)
  },[context])
  getCallback() //ここでコールバック関数を返す
  //更新
  const hundle = (e)=>{
    const {name,value} = e.target
    setItem({...d_item,[name]:value})
  } 
  //更新ボタン押下イベント
  const onSubmit = ()=>{
    const data = {
      title:d_item.title,
      description:d_item.description,
      str_status:d_item.str_status,
    }
    context.updTodo(params.id,data)
    navigate('/') //トップページに遷移
  }
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
          <>
          <div>
            <label>タイトル</label>
            <input type="text" id="title" name="title" value={d_item.title} onChange={hundle}/>
          </div>
          <div>
            <label>説明</label>
            <textarea id="description" name="description" value={d_item.description} onChange={hundle}/>
          </div>
          <div>
          <label>ステータス</label>
            <select id="status" name="str_status" onChange={hundle}>
              <option value="waiting">waiting</option>
              <option value="working">working</option>
              <option value="completed">completed</option>
              <option value="pending">pending</option>
            </select>
          </div>
          </>
      }
      <button onClick={()=>onSubmit()}>更新する</button>
    </>
	)
}

export default EditTodo;

然而,对于包含动态变化的修正页面的情况,这种方法无法适应。

方法五:利用 useEffect 钩子和 useCallback 钩子。

在这种情况下,如果根据以下答案使用`useCallback`钩子和`useEffect`钩子一起尝试使用,可以很好地处理并且使用钩子的方法也被认为是合适的,所以我认为这是最佳实践。如果只使用`useEffect`钩子,它会在`d_item`变量分配之前尝试展开JSX,但是如果使用`useCallback`钩子,它将记住筛选过程并为我们保留变量`d_item`,并且还可以通过`useEffect`钩子和`useState`钩子进行状态管理生成JSX后。

在函数组件(箭头函数)中如何使用构造函数?

import React,{ useState,useContext,useEffect,useCallback } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const EditTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  const getCallback = useCallback( async (todos)=>{
    const item = await todos.find((item)=>item.id == params.id)
    setItem(item)
  },[params.id])
  useEffect(()=>
    getCallback(context.todos) //useEffectフックから変数処理をコールバックする
   ,[context,getCallback])
  //更新
  const hundle = (e)=>{
    const {name,value} = e.target
    setItem({...d_item,[name]:value})
  } 
  //更新ボタン押下イベント
  const onSubmit = ()=>{
    const data = {
      title:d_item.title,
      description:d_item.description,
      str_status:d_item.str_status,
    }
    context.updTodo(params.id,data)
    navigate('/') //トップページに遷移
  }
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params.id != d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした</div>
        :
          <>
          <div>
            <label>タイトル</label>
            <input type="text" id="title" name="title" value={d_item.title} onChange={hundle}/>
          </div>
          <div>
            <label>説明</label>
            <textarea id="description" name="description" value={d_item.description} onChange={hundle}/>
          </div>
          <div>
          <label>ステータス</label>
            <select id="status" name="str_status" onChange={hundle}>
              <option value="waiting">waiting</option>
              <option value="working">working</option>
              <option value="completed">completed</option>
              <option value="pending">pending</option>
            </select>
          </div>
          </>
      }
      <button onClick={()=>onSubmit()}>更新する</button>
    </>
	)
}

export default EditTodo

Option 1:

第六种方法:在函数组件内直接写入(此种写法目前不推荐)。

我试着像Vue3的composition API或Svelte的setup方法一样直接编写,看看会发生什么。如果只是简单地将变量赋值并引用结果,就像详细页面一样,这种方式似乎没有问题,而且官方教程中也有实际示例。

import React,{ useState,useContext,useCallback } from 'react'
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'

const DetailTodo: React.FC = () => {
  const [d_item,setItem] = useState([])
  const context = useContext(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  const d_item = context.todos.find((item)=>item.id == params.id)

然而,如果对于像修正页面那样一旦展开在表单上的变量进行动态更改的情况,使用上述方法会导致许多麻烦的问题(无法控制在表单中的更改,但在加载时调用useState会导致无限循环错误)。最终,由于其使用受限于没有动作的详细页面等限制,所以在本案例中不推荐使用这种方法。

總結

最佳实践可能是使用 useEffect 和 useCallback 的第五种方法。不过,我个人认为使用 useMemo 的第三种方式也是可行的。虽然它可能不是最理想的使用方式,可能被视为不好的做法,但因为它比第五种方式更简单,所以我也在考虑。

如果有人能够给出明确的回答,提供意见等将非常感激。

广告
将在 10 秒后关闭
bannerAds