首页 > 解决方案 > 调用使用钩子的模块时的无限循环

问题描述

在 Main.tsx 中,我一直在使用 useApi(opts,payload),这是我制作的一个挂钩,用于订阅有效负载的更改。每当更改该变量时,它将进行 API 调用。

当我在 Main.tsx 中使用 usApi() 时(正如我已注释掉的那样),一切正常。

只有当我将此逻辑移至另一个功能组件(Auth.tsx)并尝试在 Main.tsx 中调用它时,才会遇到问题。问题是它似乎试图在无限循环中进行 API 调用,但从未抛出错误。

这里发生了什么?

主要.tsx

function Main():JSX.Element { 

  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
 
  const opts = {
    username: username,
    password: password,
    fail: ()=>null
  }

  const defaultPayload = {
    path: 'notes/validateAuth/',
      method: 'GET',
      body: null,
      callback: ()=>null
  }

  const [payload, setPayload] = useState<IPayload>(defaultPayload)
  //useApi(opts, payload) <---- THIS WORKS


  Auth() // <---- THIS DOES NOT

...

身份验证.tsx

import React, {useState} from 'react';
import useApi, {IPayload} from './hooks/useApi';
import Modal from 'react-modal'
import './modal.css';
Modal.setAppElement('#root')

function Auth():JSX.Element{

  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const opts = {
    username: username,
    password: password,
    fail: ()=>null,
  }

  const defaultPayload = {
      path:'notes/validateAuth/',
      method: 'GET',
      body: null,
      callback: ()=>null
  }

  //const _payload = (props.payload===null)?defaultPayload:props.payload
  // console.log(_payload)
  useApi(opts, defaultPayload)

  
  return(
    <></>
  )
}

export default Auth;

使用Api.tsx

如果需要调试:

import {useState, useEffect, useCallback} from 'react'
import createPersistedState from 'use-persisted-state'
import {apiUrl} from '../config'
console.log("MODE: "+apiUrl)
export interface IProps {
    username:string,
    password:string,
    fail: ()=>void
}

export interface IPayload {
    path:string,
    method:string,
    body:{}|null,
    callback: (result:any)=>any,
}

function useApi(props:IProps, payload:IPayload){

    const [accessKey, setAccessKey] = useState('')
    const useRefreshKeyState = createPersistedState('refreshKey')
    const [refreshKey, setRefreshKey] = useRefreshKeyState('')
    //const [refreshKey, setRefreshKey] = useState('')
    const [refreshKeyIsValid, setRefreshKeyIsValid] = useState<null|boolean>(null)
    // const apiUrl = 'http://127.0.0.1:8000/api/'
    // const apiUrl = '/api/'
    const [accessKeyIsValid, setAccessKeyIsValid] = useState<null|boolean>(null)

    const validHttpCodes = [200, 201, 202, 203, 204]
    
    const go = useCallback((accessKey)=>{
        const {body, method, path} = payload
        console.log('executing GO:'+accessKey)
        if(method === 'logout'){
            return logout(payload.callback)
        }
        const options = {
            method: method,
            headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer '+accessKey,
            },
            ... (body !== null) && { body: JSON.stringify(body) }
        }
        fetch(apiUrl+path,options)
        .then(response=>{
            if(!validHttpCodes.includes(response.status)){
                setAccessKeyIsValid(false)
            } else {
                setAccessKeyIsValid(true)
                response.json()
                .then(response=>{
                    payload.callback(response)
                })
            }
        })
    },[payload])

    function logout(callback:(response:null)=>void){
        setRefreshKey('')
        setAccessKey('')
        setRefreshKeyIsValid(null)
        setAccessKeyIsValid(null)
        callback(null)
    }

    useEffect(()=>{
        if(accessKeyIsValid===false){
            console.log('access key is false')
            // We tried to make a request, but our key is invalid.
            // We need to use the refresh key
            const options = {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', },
                body: JSON.stringify( {'refresh': refreshKey} ),
            }
            fetch(apiUrl+'token/refresh/', options)
            .then(response=>{
                if(!validHttpCodes.includes(response.status)){
                    setRefreshKeyIsValid(false)
                    // this needs to trigger a login event
                } else {
                    response.json()
                    .then(response=>{
                        setRefreshKeyIsValid(true)
                        setAccessKey(response.access)
                        // setAccessKeyIsValid(true)
                    })
                }
            })
        }
    },[accessKeyIsValid])


    useEffect(()=>{
        console.log('responding to change in access key:'+accessKey)
        go(accessKey)
    },[accessKey,payload])

    useEffect(()=>{
        if(refreshKeyIsValid===false){
            // even after trying to login, the RK is invalid
            // We must straight up log in.
            const options = {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    username: props.username,
                    password: props.password,
                 })
            }
            fetch(apiUrl+'token/', options)
            .then(response=>{
                console.log(response.status)
                if(!validHttpCodes.includes(response.status)){
                    setAccessKeyIsValid(null)
                    setRefreshKeyIsValid(null)
                    props.fail()
                    console.log('total fail')
                }
                else { 
                    console.log('login works')
                    response.json()
                    .then(response=>{
                        setAccessKey(response.access)
                        setRefreshKey(response.refresh)
                        // setRefreshKeyIsValid(true)
                        // setAccessKeyIsValid(true) // Commenting this out disables the loop
                    })
                }
            })
        }

        
    },[refreshKeyIsValid])

};

export default useApi

注意:我有理由需要排除这个逻辑,这在我的删节代码中并不明显。

标签: javascriptreactjstypescriptreact-hooks

解决方案


我们已经在聊天中谈了一点,以便更多地了解这里发生的事情。我稍后会编辑您的问题以删除不相关的代码,以便任何遇到问题的人都更容易理解。

您的原始代码执行以下操作:

  • defaultProps在功能组件中实例化一个对象 ( )
  • 将该对象作为default调用useState()传递
  • 将状态值传递给你的钩子,然后在useEffect()钩子的依赖数组中使用

你的重构改变了它,所以你直接将 defaultProps 引用传递到你的钩子中,而不像以前那样通过useState()钩子。

关于 React 组件需要了解的重要一点是,尽管它们的执行模式在某种程度上是可预测的,但作为开发人员,这是您无法控制的,因此您应该在编写组件时假设它们可能随时重新渲染。

在功能组件中,每次重新渲染都是具有新范围的新功能执行。如果你在这个范围内实例化一个对象,那就是一个新的引用变量,每次都指向不同的内存区域

然后在 hook 的依赖数组中使用 this 的问题useEffect()是,由于 this 在每次渲染时都是一个新变量,这意味着每次重新渲染组件时,useEffect hook 会看到不同的引用,并导致效果回调被再次执行。这通常不是预期的行为。


const component = () => {
   
   /*
    * Each time this component
    * renders, this object is created again
    * in (probably) a different area of memory
    */
   const obj = {
      foo: 'bar'
   }

   /*
    * Although 'obj' *appears* to be the same here since it is in fact
    * a different reference, this console.log statement is going 
    * to execute every single time the component re-renders
    */
   useEffect(() => {
      console.log('running effect!')
   }, [obj])

   return <div></div>
}

那么解决方案是什么?

原始代码首先传递defaultPayloaduseState调用中。这是正确的做法。这样做的原因是因为组件状态值在渲染之间确实保持相同的引用,因此可以安全地在useEffect()依赖数组中使用。


const component = () => {
   
   /*
    * still a different reference on each render
    */
   const obj = {
      foo: 'bar'
   }

   const [objState, setObjState] = React.useState(obj);

   /*
    * Although `obj` is a difference reference on every render
    * objState is *not*, so our `console.log` will now only execute
    * when objState changes
    */
   useEffect(() => {
      console.log('running effect!')
   }, [objState])

   return <div></div>
}

替代(但有点无意义)解决方案

请注意,您还可以通过简单地在功能组件范围之外(即在模块全局范围内)实例化对象来解决此问题中遇到的非常具体的问题。但是,一旦您希望能够更改它,您就需要将它放入状态变量中(这个名称defaultPayload暗示您会这样做)

/*
 * Definitions made within the module global scope
 * are instantiated only once, when the module is imported
 */
const obj = {
  foo: 'bar'
}

const component = () => {
   
   /*
    * useEffect hook will now only ever fire once
    * since `obj` is never going to change
    */
   useEffect(() => {
      console.log('running effect!')
   }, [obj])

   return <div></div>
}

推荐阅读