javascript - 如何防止对未安装组件的异步请求进行 React 状态更新?
问题描述
我正在开发一个 mernstack 应用程序,其中我有一个用于 API 请求的自定义钩子,其中包含加载到上下文 api 中的 useReducer 状态和调度函数。通常 GET 请求在页面加载时运行顺利,但每次我使用 POST、PATCH、PUT 和 DELETE 请求函数时,它都会导致组件卸载并出现此错误:
警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
每当我刷新页面并查看更改时,错误就会消失。如何防止对未安装组件的异步请求进行 React 状态更新?
数据库设置
const mongodb = require('mongodb');
const { MongoClient, ObjectID } = mongodb;
require('dotenv').config();
const mongourl = process.env.MONGO_URI;
const db_name = process.env.DB_NAME;
let db;
async function startConnection(cb) {
let client;
try {
client = await MongoClient.connect(mongourl, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
db = client.db(db_name);
await cb();
} catch (err) {
await cb(err);
}
}
const getDb = () => {
return db;
};
const getPrimaryKey = (_id) => {
return ObjectID(_id);
};
module.exports = { db, startConnection, getDb, getPrimaryKey };
服务器:
const express = require('express');
require('dotenv').config();
const port = process.env.PORT || 8000;
const db = require('./db');
const db_col = process.env.DB_COL;
const router = express.Router();
let status;
db.startConnection((err) => {
if (err) {
status = `Unable to connect to the database ${err}`;
console.log(status);
} else {
status = 'Connected to the database';
console.log(status);
}
});
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/list', router);
router.get('/', (req, res) => {
db.getDb()
.collection(db_col)
.find({})
.toArray((err, docs) => {
if (err) {
console.log(err);
}
res.json(docs);
console.log(docs);
});
});
router.post('/', (req, res) => {
const newlist = req.body;
const { list_name, list_items } = newlist;
db.getDb()
.collection(db_col)
.insertOne({ list_name, list_items }, (err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
});
});
router.patch('/:id', (req, res) => {
const paramID = req.params.id;
const listname = req.body.list_name;
db.getDb()
.collection(db_col)
.updateOne(
{ _id: db.getPrimaryKey(paramID) },
{ $set: { list_name: listname } },
(err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
}
);
});
router.put('/:id', (req, res) => {
const paramID = req.params.id;
const listitems = req.body.list_items;
db.getDb()
.collection(db_col)
.updateOne(
{ _id: db.getPrimaryKey(paramID) },
{ $set: { list_items: listitems } },
(err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
}
);
});
router.delete('/:id', (req, res) => {
const paramID = req.params.id;
db.getDb()
.collection(db_col)
.deleteOne({ _id: db.getPrimaryKey(paramID) }, (err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
});
});
app.listen(port, console.log(`Server listening to port: ${port}`));
行动:
import { LOADING, PROCESSING_REQUEST, HANDLING_ERROR } from './actionTypes';
const loading = () => {
return {
type: LOADING,
};
};
const processingRequest = (params) => {
return {
type: PROCESSING_REQUEST,
response: params,
};
};
const handlingError = () => {
return {
type: HANDLING_ERROR,
};
};
export { loading, processingRequest, handlingError };
减速器:
import {
LOADING,
PROCESSING_REQUEST,
HANDLING_ERROR,
} from './actions/actionTypes';
export const initialState = {
isError: false,
isLoading: false,
data: [],
};
const listReducer = (state, { type, response }) => {
switch (type) {
case LOADING:
return {
...state,
isLoading: true,
isError: false
};
case PROCESSING_REQUEST:
return {
...state,
isLoading: false,
isError: false,
data: response,
};
case HANDLING_ERROR:
return {
...state,
isLoading: false,
isError: true
};
default:
throw new Error();
}
};
export default listReducer;
API 请求的自定义 Hook:
import { useEffect, useCallback, useReducer } from 'react';
import axios from 'axios';
import listReducer, { initialState } from '../../context/reducers/reducers';
import {
loading,
processingRequest,
handlingError,
} from '../../context/reducers/actions/actionCreators';
const useApiReq = () => {
const [state, dispatch] = useReducer(listReducer, initialState);
const getRequest = useCallback(async () => {
dispatch(loading());
try {
const response = await axios.get('/list');
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const postRequest = useCallback(async (entry) => {
dispatch(loading());
try {
const response = await axios.post('/list', entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const patchRequest = useCallback(async (id, updated_entry) => {
dispatch(loading());
try {
const response = await axios.patch(`/list/${id}`, updated_entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const putRequest = useCallback(async (id, updated_entry) => {
dispatch(loading());
try {
const response = await axios.put(`/list/${id}`, updated_entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const deleteRequest = useCallback(async (id) => {
dispatch(loading());
try {
const response = await axios.delete(`/list/${id}`);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
return [
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
];
};
export default useApiReq;
上下文 API
import React, { createContext } from 'react';
import useApiReq from '../components/custom-hooks/useApiReq';
export const AppContext = createContext();
const AppContextProvider = (props) => {
const [
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
] = useApiReq();
return (
<AppContext.Provider
value={{
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
}}
>
{props.children}
</AppContext.Provider>
);
};
export default AppContextProvider;
应用程序:
import React from 'react';
import AppContextProvider from './context/AppContext';
import Header from './components/header/Header';
import Main from './components/main/Main';
import './stylesheets/styles.scss';
function App() {
return (
<AppContextProvider>
<div className='App'>
<Header />
<Main />
</div>
</AppContextProvider>
);
}
export default App;
Main: 这是 GET 请求在初始加载时发生的地方。
import React, { useEffect, useContext } from 'react';
import { AppContext } from '../../context/AppContext';
import Sidebar from '../sidebar/Sidebar';
import ParentListItem from '../list-templates/ParentListItem';
function Main() {
const { state, getRequest } = useContext(AppContext);
const { isError, isLoading, data } = state;
useEffect(() => {
getRequest();
}, [getRequest]);
return (
<main className='App-body'>
<Sidebar />
<div className='list-area'>
{isLoading && (
<p className='empty-notif'>Loading data from the database</p>
)}
{isError && <p className='empty-notif'>Something went wrong</p>}
{data.length == 0 && <p className='empty-notif'>Database is empty</p>}
<ul className='parent-list'>
{data.map((list) => (
<ParentListItem key={list._id} {...list} />
))}
</ul>
</div>
</main>
);
}
export default Main;
侧边栏
import React, { useState } from 'react';
import Modal from 'react-modal';
import AddList from '../modals/AddList';
import DeleteList from '../modals/DeleteList';
/* Modal */
Modal.setAppElement('#root');
function Sidebar() {
const [addModalStatus, setAddModalStatus] = useState(false);
const [deleteModalStatus, setDeleteModalStatus] = useState(false);
const handleAddModal = () => {
setAddModalStatus((prevState) => !prevState);
};
const handleDeleteModal = () => {
setDeleteModalStatus((prevState) => !prevState);
};
return (
<aside className='sidebar'>
<nav className='nav'>
<button className='btn-rec' onClick={handleAddModal}>
Add
</button>
<button className='btn-rec' onClick={handleDeleteModal}>
Delete
</button>
</nav>
<Modal isOpen={addModalStatus} onRequestClose={handleAddModal}>
<header className='modal-header'>Create New List</header>
<div className='modal-body'>
<AddList exitHandler={handleAddModal} />
</div>
<footer className='modal-footer'>
<button onClick={handleAddModal} className='btn-circle'>
×
</button>
</footer>
</Modal>
<Modal isOpen={deleteModalStatus} onRequestClose={handleDeleteModal}>
<header className='modal-header'>Delete List</header>
<div className='modal-body'>
<DeleteList exitHandler={handleDeleteModal} />
</div>
<footer className='modal-footer'>
<button onClick={handleDeleteModal} className='btn-circle'>
×
</button>
</footer>
</Modal>
</aside>
);
}
export default Sidebar;
添加模态 这是调用发布请求的地方
import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';
const AddList = ({ exitHandler }) => {
const { postRequest } = useContext(AppContext);
const [newList, setNewList] = useState({});
const inputRef = useRef(null);
/* On load set focus on the input */
useEffect(() => {
inputRef.current.focus();
}, []);
const handleAddList = (e) => {
e.preventDefault();
const new_list = {
list_name: inputRef.current.value,
list_items: [],
};
setNewList(new_list);
};
const handleSubmit = (e) => {
e.preventDefault();
postRequest(newList);
exitHandler();
};
return (
<form onSubmit={handleSubmit} className='generic-form'>
<input
type='text'
ref={inputRef}
placeholder='List Name'
onChange={handleAddList}
/>
<input type='submit' value='ADD' className='btn-rec' />
</form>
);
};
export default AddList;
删除模式 这是调用删除请求的地方。
import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';
const DeleteList = ({ exitHandler }) => {
const { state, deleteRequest } = useContext(AppContext);
const { data } = state;
const selectRef = useRef();
const [targetListId, setTargetListId] = useState();
useEffect(() => {
selectRef.current.focus();
}, []);
useEffect(() => {
setTargetListId(data[0]._id);
}, [data]);
const handleDeleteList = (e) => {
e.preventDefault();
deleteRequest(targetListId);
exitHandler();
};
const handleChangeList = (e) => {
setTargetListId(e.target.value);
};
return (
<form onSubmit={handleDeleteList} className='generic-form'>
<label>
<select
ref={selectRef}
value={targetListId}
onChange={handleChangeList}
className='custom-select'
>
{data.map((list) => (
<option key={list._id} value={list._id}>
{list.list_name}
</option>
))}
</select>
</label>
<input type='submit' value='DELETE' className='btn-rec' />
</form>
);
};
export default DeleteList;
父列表: 这是调用 PUT、PATCH 请求的地方
import React, { useContext, useState, useEffect, useRef } from 'react';
import { FaPen, FaCheck } from 'react-icons/fa';
import ChildListItem from './ChildListItem';
import { AppContext } from '../../context/AppContext';
import displayDate from '../../utilities/utilities';
import { v4 } from 'uuid';
function ParentListItem({ _id, list_name, list_items }) {
const { patchRequest, putRequest } = useContext(AppContext);
const [activeListItems, setActiveListItems] = useState([]);
const [completedListItems, setCompletedListItems] = useState([]);
const [listItems, setListItems] = useState({});
const [disabledInput, setDisabledInput] = useState(true);
const [title, setTitle] = useState({});
const [status, setStatus] = useState(false);
const titleRef = useRef();
const { day, date, month, year, current_time } = displayDate();
const handleCreateNewItem = (e) => {
const newItem = {
item_id: v4(),
item_name: e.target.value,
item_date_created: `${day}, ${date} of ${month} ${year} at ${current_time}`,
isComplete: false,
};
const new_list_items = [...list_items, newItem];
setListItems({ list_items: new_list_items });
};
/* Handles the edit list title button */
const toggleEdit = () => {
setDisabledInput(!disabledInput);
};
/* Handles the edit list title button */
const toggleStatus = (item_id) => {
const target = list_items.find((item) => item.item_id == item_id);
let updated_list = [...list_items];
updated_list.map((list) => {
if (list == target) {
list.isComplete = !list.isComplete;
}
});
const update = { list_items: updated_list };
putRequest(_id, update);
};
/* Handles the edit list title button */
const deleteItem = (item_id) => {
const target = list_items.find((item) => item.item_id == item_id);
let updated_list = [...list_items].filter((list) => {
if (target.isComplete == true) {
return list !== target;
}
});
const update = { list_items: updated_list };
putRequest(_id, update);
};
/* Handles the edit list tile input */
const handleTitleChange = (e) => {
const newTitle = { list_name: e.target.value };
setTitle(newTitle);
};
/* Handles the submit or dispatched of edited list tile*/
const handleUpdateTitle = (e) => {
e.preventDefault();
patchRequest(_id, title);
setDisabledInput(!disabledInput);
};
const handleSubmitItem = (e) => {
e.preventDefault();
putRequest(_id, listItems);
[e.target.name] = '';
};
useEffect(
(e) => {
if (disabledInput === false) titleRef.current.focus();
},
[disabledInput]
);
useEffect(() => {
setTitle(list_name);
}, [list_name]);
useEffect(() => {
/* On load filter the active list */
let active_list_items = list_items.filter(
(item) => item.isComplete === false
);
setActiveListItems(active_list_items);
}, [list_items]);
useEffect(() => {
/* On load filter the completed list */
let completed_list_items = list_items.filter(
(item) => item.isComplete === true
);
setCompletedListItems(completed_list_items);
}, [list_items]);
return (
<li className='parent-list-item'>
<header className='p-li-header'>
<input
type='text'
className='edit-input'
name='newlist'
ref={titleRef}
defaultValue={list_name}
onChange={handleTitleChange}
disabled={disabledInput}
/>
{disabledInput === true ? (
<button className='btn-icon' onClick={toggleEdit}>
<FaPen />
</button>
) : (
<form onSubmit={handleUpdateTitle}>
<button className='btn-icon' type='submit'>
<FaCheck />
</button>
</form>
)}
</header>
<div id={_id} className='p-li-form-container'>
<form className='generic-form clouds' onSubmit={handleSubmitItem}>
<input
type='text'
placeholder='Add Item'
name='itemname'
onChange={handleCreateNewItem}
/>
<input type='submit' value='+' className='btn-circle' />
</form>
</div>
<div
className={list_items.length === 0 ? 'p-li-area hidden' : 'p-li-area'}
>
<section className='pi-child-list-container'>
<h6>Active: {activeListItems.length}</h6>
{activeListItems.length === 0 ? (
<p className='empty-notif'>List is empty</p>
) : (
<ul className='child-list'>
{activeListItems.map((list) => (
<ChildListItem
key={list.item_id}
{...list}
list_id={_id}
toggleStatus={toggleStatus}
deleteItem={deleteItem}
/>
))}
</ul>
)}
</section>
<section className='pi-child-list-container'>
<h6>Completed: {completedListItems.length}</h6>
{completedListItems.length === 0 ? (
<p className='empty-notif'>List is empty</p>
) : (
<ul className='child-list'>
{completedListItems.map((list) => (
<ChildListItem
key={list.item_id}
{...list}
list_id={_id}
toggleStatus={toggleStatus}
deleteItem={deleteItem}
/>
))}
</ul>
)}
</section>
</div>
</li>
);
}
export default ParentListItem;
子列表
import React from 'react';
import { IconContext } from 'react-icons';
import { FaTrashAlt, FaRegCircle, FaRegCheckCircle } from 'react-icons/fa';
function ChildListItem({
item_name,
item_id,
item_date_created,
isComplete,
toggleStatus,
deleteItem,
}) {
const handleIsComplete = (e) => {
e.preventDefault();
toggleStatus(item_id);
};
const handleDeleteItem = (e) => {
e.preventDefault();
deleteItem(item_id);
};
return (
<li className='c-li-item' key={item_id}>
<div className='c-li-details'>
<p className='item-name'>{item_name}</p>
<p className='date-details'>Date created: {item_date_created}</p>
</div>
<div className='c-li-cta'>
<label htmlFor={item_id} className='custom-checkbox-label'>
<input
type='checkbox'
id={item_id}
checked={isComplete}
onChange={handleIsComplete}
/>
<span className='btn-icon'>
<IconContext.Provider
value={{ className: 'react-icon ri-success' }}
>
{isComplete === false ? <FaRegCircle /> : <FaRegCheckCircle />}
</IconContext.Provider>
</span>
</label>
<button
className='btn-icon btn-delete'
disabled={!isComplete}
onClick={handleDeleteItem}
>
<IconContext.Provider
value={{
className:
isComplete === false
? 'react-icon ri-disabled'
: 'react-icon ri-danger',
}}
>
<FaTrashAlt />
</IconContext.Provider>
</button>
</div>
</li>
);
}
export default ChildListItem;
解决方案
出现警告是因为您的组件收到了响应但它已经被卸载(停止渲染)
要解决此问题,您必须使用 useEffect() 的清理功能(通过返回取消功能)和 axios 像下面的示例这样卸载组件后取消请求
useEffect( () => {
const CancelToken = axios.CancelToken;
let cancel;
const callAPI = async () => {
try {
let res = await axios.post(`.....`, { cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
}) });
}
catch (err) {
console.log(err)
}
}
callAPI();
return (cancel);
}, []);
您可以在 axios 文档中阅读更多内容:https ://github.com/axios/axios 但是,请记住,这只解决了警告,而不是它在您的发布请求后重定向的原因。
推荐阅读
- debugging - LuaJIT:脚本访问全局变量多少次?
- javascript - 从 prop 事件处理程序设置时,React 状态重置
- linux - 公钥不可用:NO_PUBKEY F76221572C52609D
- pandas - Pandas - 使用 pivot_table 将 1 列中的每个唯一元素转换为唯一列
- javascript - 如何从其类中获取 HTML 元素的 CSS 属性?
- airflow-scheduler - Alteryx 的气流操作员
- java - 计数为零时如何不返回任何内容?
- spyder - 使用 Python 绘制 vtk 文件
- javascript - 引导多选 selectAll 不适用于 Knockout.js,除非使用 setTimeout
- java - 在Java中获取浮点数小数点前后位数的最有效方法是什么?