在 useEffect 中使用 useRef current 的问题

Posted

技术标签:

【中文标题】在 useEffect 中使用 useRef current 的问题【英文标题】:Problem with using useRef current in useEffect 【发布时间】:2021-07-03 01:40:43 【问题描述】:

我有以下问题:目前我正在尝试编写一个 customHook,我可以使用它进行所有后端操作。这样做的目的是让我的代码看起来更干净、更容易理解并且更少写。

要理解这个问题,我必须先解释几件事。首先,我正在使用 JWT 令牌并执行一些操作,您需要使用身份验证标头中的令牌进行身份验证。如果我在尝试发布内容时遇到错误,我的函数应该刷新 JWT 令牌并再次调用该函数,如果第一次发生身份验证错误。因为我正在使用功能组件,而 customHook(我认为)只能以功能组件的方式使用钩子,所以在将状态设置为当前错误值后,我必须再次调用前一个函数。因此,我将以前的函数及其参数保存在 useRefs 中,但是当触发 useEffect 时,ref.current 值没有它们应该具有的值。

在下面的代码中只包含了这个问题需要的操作:

使用后端:

import axios from "axios";
import  useEffect, useRef, useState  from "react";

const BASE_URL = 'https://localhost:44372/api/';
const R = 'Requests/'; const M = 'Movies/'; const U = 'Users/';


const defaultOperations = (controller) => 
    return 
       post: (object) => buildParams((`$controller/Post$controller.substring(0, controller.length-1)`), 'post', true, object),
       put: (object, id) => buildParams((`$controller/Put$controller.substring(0, controller.length-1)/$id`), 'put', true, object),
       delete: (id) => buildParams((`$controller/Delete$controller.substring(0, controller.length-1)/$id`), 'delete', true),
       getAll: () => buildParams((`$controller/Get$controller`), 'get', false),
       getByID: (id) => buildParams((`$controller/Get$controller.substring(0, controller.length-1)/$id`), 'get', false),
    


const buildParams = (url, type, header, param) => 
    return url: url, type: type, header: header, param: param;



export const CONTROLLERS = 
    REQUESTS: 
        post: (object) => defaultOperations('Requests').post(object),
        put: (object, id) => defaultOperations('Requests').put(object, id),
        delete: (id) => defaultOperations('Requests').delete(id),
        getAll: () => defaultOperations('Requests').getAll(),
        getByID: (id) => defaultOperations('Requests').getByID(id),
        getRequestsByUser: () => buildParams(`$RGetRequestsByUser`, 'post', true, accessToken: localStorage.accessToken),
    
    ,

    USERS: 
        refreshToken: () => buildParams(`$URefreshToken`, 'post', false, accessToken: localStorage.accessToken, refreshToken: localStorage.refreshToken),
        login: (mail, password) => buildParams(`$ULogin`, 'post', false, mail: mail, password: password),
        register: (object) => buildParams(`$URegister`, 'post', false, object),
        getUserByAccessToken: () => buildParams(`$UGetUserByAccessToken`, 'post', true, accessToken: localStorage.accessToken),
        authentificate: () => buildParams('authentificate', 'special', true, undefined),
        getAll: () => defaultOperations('Users').getAll(),
        getByID: (id) => defaultOperations('Users').getByID(id),
        post: (object) => defaultOperations('Users').post(object),
        put: (object, id) => defaultOperations('Users').put(object, id),
        delete: (id) => defaultOperations('Users').delete(id),

    ,



export const  useBackend = (error, setError, initValue) => 
    const [values, setValues] = useState([]);
    const lastFunction = useRef();
    const lastParameter = useRef();


     function selectFunction(objc) 
        switch(objc.type) 
            case 'get':  buildGetAndFetch(objc.url, objc.param, objc.header); break;
            case 'post':  buildPostAndFetch(objc.url, objc.param, objc.header);break;
            case 'put':  buildPutAndFetch(objc.url, objc.param, objc.header);break;
            case 'delete':  buildDeleteAndFetch(objc.url, objc.param, objc.header);break;
            case 'special': authentificate();break;
            default: console.log("Error in Switch");
        
    
    if(initValue!==undefined) setValues(initValue);


  


     function buildPostAndFetch(url, param, header) 
         let _param = param;
         if(param?.accessToken !== undefined) 
            _param = accessToken: localStorage.accessToken;
            if(param?.refreshToken) 
                _param = ...param, refreshToken: localStorage.refreshToken
            
         'GetUserByAccessToken'));
        const finalurl = `$BASE_URL$url`;
        if(header) 
            axios.post(finalurl, _param, headers: 
                'Authorization': `Bearer $(localStorage.accessToken)`
            )
            .then(res => 
                response(res);
            )
            .catch(err =>         
               authError(buildPostAndFetch, url, param, header);           
            )
        
        else 
            axios.post(finalurl, param)
            .then(res => 
                response(res);
            )
            .catch(err => 
                setError(true);
            )
        
    

    

    
    useEffect(() => 
    , [values])

     function buildDeleteAndFetch(url, param, header) 
        const finalurl = `$BASE_URL$url`;
        if(header) 
            axios.delete(finalurl, param, headers: 'Authorization': `Bearer $(localStorage.accessToken)`)
            .then(res => 
                setValues(values.filter(item => item.requestID !== param));
                setError(false);
            )
            .catch(err => 
                 authError(buildDeleteAndFetch, url, param, header);
            )
        
        else 
            axios.delete(finalurl, param)
            .then(res => 
                setValues(values.filter(item => item.requestID !== param));
                setError(false);
            )
            .catch(err => 
                setError(true);
            )
        
    

    function response(res) 
        setValues(res.data)
        setError(false);
    

      function refreshToken(err) 
        const finalurl= `$BASE_URL$URefreshToken`;
       
        axios.post(finalurl, accessToken: localStorage.accessToken, refreshToken: localStorage.refreshToken)
        .then(res => 
            localStorage.accessToken = res.data.accessToken;
            setError(err);
        )
        .catch(err => 
            localStorage.accessToken = undefined;
            localStorage.refreshToken = undefined;
            setError(err);
        );
    


     function authError(funktion223, url, param, header) 
        if(error!==true) 
            lastFunction.current = funktion223;
            lastParameter.current = [url, param, header];
            refreshToken(true);
        
    

    useEffect(() => 
        if(error===true) 
            if(localStorage.accessToken !== undefined && localStorage.refreshToken !== undefined) 
                    const lastfunc = lastFunction.current;
                    const lastparams = lastParameter.current;
                    if(lastfunc !== undefined && lastparams.length > 0 ) 
                        console.log(lastfunc);
                        console.log(lastparams[0]);
                        lastfunc(lastparams[0], lastparams[1], lastparams[2]);
                    
            
        
    , [error])
    
    return [values, 
            (objc) => selectFunction(objc)];


组件说明: 我是 userSettings 组件,当 componentsDidMount 调用钩子以检查用户是否登录时,如果没有,尝试从后端获取其他数据没有任何意义。如果他已登录或至少他的令牌已过期,它将被刷新。在 UserRequests 组件中,当没有错误时,将获取用户的请求。

(我不知道你是否需要这些信息,也许你认为自定义钩子是正确的,而我只是在使用它们的组件中犯了错误)

用户设置:

import React,  useEffect, useState  from 'react';
import Row, Col from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../../App.css';
import ProfileSettings from './profileSettings';
import SettingsChooser from './settingsChooser';
// import SettingRoutings from '../settingRoutings';
import BrowserRouter as Router, useLocation, useParams from 'react-router-dom';
// import Routings from '../Routings.js';
import UserRequests from './userRequests';
import useAuth from '../../API/useAuthentification';
import  CONTROLLERS, useBackend  from '../../hooks/useBackend';

function UserSettings(user) 

    const title: path = useParams();
    const [authError, setAuthError] = useState(false);
    const [userValues, authentificate] = useBackend(authError, setAuthError, user);
    let component;
    

    useEffect(() => 
        console.log('render');
        authentificate(CONTROLLERS.USERS.getUserByAccessToken());
    , [])

    useEffect(() => 
        console.log(userValues);
    , [userValues])

    if(path ==='logout') 
        // localStorage.accessToken = undefined;
        // localStorage.refreshToken = undefined
    
    else if(path === 'deleteaccount') 
        //TODO
        //Implement  
    
        
   component = <UserRequests user=userValues setAuthError=setAuthError authError=authError/>



    return (
            <div classname="">
            <Row className="">
            <Col className="formsettings2"   md= span: 3, offset: 1>
                <SettingsChooser/>
            </Col>
           <Col  className="ml-5 formsettings2"md= span: 6>
                     authError ? <p>No Access, please Login first</p> : component                 
            </Col>
            </Row>
            </div>
    );


export default UserSettings;

请求:

import React,  useEffect, useState  from 'react';
import Table from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import createAPIEndpoint from '../../API/callBackendAPI';
import toast  from 'react-toastify';
import Loading from '../Alerts/loading';
import notify from '../Alerts/toasts';
import  ToastProvider  from 'react-toast-notifications';
import useAuth from '../../API/useAuthentification';
import  useHistory  from 'react-router';
import  CONTROLLERS, useBackend  from '../../hooks/useBackend';

toast.configure(
    
);

function UserRequests(user, authError, setAuthError) 

    const headers = ['ID', 'Title', 'Release Date', 'Producer', 'Director', 'Status', 'UTC Time', '#', '#'];
    const history = useHistory();
    const [requests, requestActions] = useBackend(authError, setAuthError);
    const [loaded, setLoaded] = useState(false);

    useEffect(() => 
        if(requests.length > 0) setLoaded(true);
    , [requests])
    useEffect(() =>  
       if(authError === false) requestActions(CONTROLLERS.REQUESTS.getRequestsByUser());
    , []);


    
    const handleDelete = (e) => 
        requestActions(CONTROLLERS.REQUESTS.delete(e));
    

    const handleUpdate = (e) => 
        console.log(e);
        sessionStorage.movie = JSON.stringify(e);
        history.push('/updateRequest');
    



    if(loaded===false) return <Loading/>

    return(
      <ToastProvider>
      <Table bordered  hover responsive="md">
          <thead>
              <tr>
                headers.map((item, index) => 
                   return( <th className="text-center" key=index>item</th> );
                )
              </tr>
          </thead>
          <tbody>
                requests.map((item, index) =>
                    return(
                    <tr>
                        <td>index + 1</td>
                        <td>item.movie.movieTitle</td>
                        <td>item.movie.movieReleaseDate</td>
                        <td>item.movie.movieProducer</td>
                        <td>item.movie.movieDirector</td>
                        <td>(item.requestStatus === 1 ? 'Success' : item.requestStatus ===2 ? 'Pending' : 'Denied')</td>
                        <td className="col-md-3">item.requestDate</td>
                        <td><span  onClick=() => handleDelete(item.requestID)><i  className="fas fa-times"></i></span></td>
                        <td><span  onClick=() => handleUpdate(item.movie)><i  className="fa fa-pencil-alt"></i></span></td>
                    </tr>);
                )
          </tbody>
      </Table>
      </ToastProvider>
    );


export default UserRequests

【问题讨论】:

【参考方案1】:

useEffect 运行 onmount

当您使用 customHook useBackenderror === true 并且您在 localStorage 中有令牌时,它将尝试使用 lastFunctionlastParameter 引用,但您尚未启动它们。

 useEffect(() => 
    if(error===true)  //could use if(error) if error is boolean
        if(localStorage.accessToken !== undefined && localStorage.refreshToken !== undefined) 
                const lastfunc = lastFunction.current;
                const lastparams = lastParameter.current;
                if(lastfunc !== undefined && lastparams.length > 0 ) 
                    console.log(lastfunc);
                    console.log(lastparams[0]);
                    lastfunc(lastparams[0], lastparams[1], lastparams[2]);
                
        
    
, [error])

如果您不想让useEffect 运行onmount,您可以使用isMounted 标志,此处已回答:

Stop useEffect from running on mount

【讨论】:

非常感谢您的回答,但我真的不明白为什么它们还没有初始化。我知道 useEffects 在挂载时运行,但是这个显式操作只在我的错误为真并且在设置 lastfunction 和 lastparams 后才设置为 true 时运行。 我试过这个解决方案,但还是不行。在 authError 中,ref 属性已正确更改,但是正如您提到的,当调用 useEffect 时,refs 没有初始化,但正如我所提到的,我不知道为什么。在我看来,isMounted 标志仅提供 useEffect 中的代码不会在初始渲染时调用,但我真的不知道它应该如何避免 useEffect 在 refs 初始化并正确设置后使用这些值。可能我只是理解不正确,思维有误,但如果您能更详细地向我解释,我将不胜感激 在您发送请求之前,请验证您的令牌是否有效,如果它已过期,您可以通过刷新令牌来处理它,然后再发送您的请求。你可以做类似这样的事情:***.com/questions/44982412/… 那么你就不需要存储你的最后一个请求,在发送请求之前处理令牌。

以上是关于在 useEffect 中使用 useRef current 的问题的主要内容,如果未能解决你的问题,请参考以下文章

在 Div 中添加子元素时,虽然使用 UseEffect 触发,但到达 useRef() 当前属性返回未知

如何在 UseEffect 中将事件侦听器添加到 UseRefs

使用 useeffect 和 useref TypeError 时出错:无法读取 null 的属性“getBoundingClientRect”

UseEffect 中的 UseRef 问题

React useRef 在运行 useEffect 以填充初始状态后返回 null

反应 useEffect 和 useRef