首页 > 解决方案 > 如何防止对未安装组件的异步请求进行 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'>
            &times;
          </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'>
            &times;
          </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;

标签: javascriptreactjsmern

解决方案


出现警告是因为您的组件收到了响应但它已经被卸载(停止渲染)

要解决此问题,您必须使用 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 但是,请记住,这只解决了警告,而不是它在您的发布请求后重定向的原因。


推荐阅读