javascript - 调用使用钩子的模块时的无限循环
问题描述
在 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
注意:我有理由需要排除这个逻辑,这在我的删节代码中并不明显。
解决方案
我们已经在聊天中谈了一点,以便更多地了解这里发生的事情。我稍后会编辑您的问题以删除不相关的代码,以便任何遇到问题的人都更容易理解。
您的原始代码执行以下操作:
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>
}
那么解决方案是什么?
原始代码首先传递defaultPayload
到useState
调用中。这是正确的做法。这样做的原因是因为组件状态值在渲染之间确实保持相同的引用,因此可以安全地在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>
}
推荐阅读
- windows - 如何在 Windows 上使用 OpenCL 进入正在运行的“hello world”?
- swift - 如何在 SwiftUI 中使用拖放手势?
- postgresql - 如何将 postgreSQL 设置为比我的计算机拥有更多 RAM 的缓冲区?
- jakarta-ee - 如何在每次数据库更改时发送 SSE
- java - 如何在后台收听屏幕触摸
- orm - 如何避免聚合依赖于外部包含?
- sql - 获取在sql server中以逗号分隔的字符串传递所有ID的所有记录
- r - 如何将图例值显示为百分比
- java - javax.net.ssl.SSLHandshakeException:sun.security.validator.ValidatorException:PKIX 路径构建
- android - 如图所示,底部栏的名称是什么?