React useEffect 清理函数意外调用

React useEffect cleanup function unexpectedly called

我正在创建自定义挂钩以在表单提交时获取 api,我在 useEffect 挂钩内进行 api 调用,并且我有一个 reducer 来处理挂钩的状态。 其中一个状态是 trigger 首先设置为 false,它控制 useEffect 是否执行任何操作,重点是钩子 returns 一个翻转 trigger 值的函数,它仅在您时触发 useEffect调用这个函数。 问题是在 api 调用期间调用了 useEffect 的清理函数,即使组件显然仍处于安装状态。

清理函数似乎被触发了,因为我根据它以前的值设置了 trigger 的值,当我将 trigger 设置为一个固定值时,清理函数没有被调用但是我输了我的功能

const fetchReducer = (state, action) => {
    switch (action.type) {
        case 'FETCH_TRIGGER':
            return {
                ...state,
                trigger: !state.trigger
            }
        case 'FETCH_INIT':
            return {
                ...state,
                isLoading: true,
                isError: false
            };
        case 'FETCH_SUCCESS':
            return {
                ...state,
                isLoading: false,
                isError: false,
                datas: action.payload,
            };
        case 'FETCH_FAILURE':
            return {
                ...state,
                isLoading: false,
                isError: true,
            };
        default:
            throw new Error();
    }
}

const useFetchApi = (query, initialData = []) => {
    let isCancelled = false;
    const [state, dispatch] = useReducer(fetchReducer, {
        isLoading: false,
        isError: false,
        datas: initialData,
        trigger: false
    });
    const triggerFetch = _ => dispatch({ type: 'FETCH_TRIGGER' });
    const cancel = _ => { console.log("canceling");isCancelled = true };

    useEffect(_ => {
        if (!state.trigger)
            return;
        triggerFetch();
        (async _ => {
            dispatch({ type: 'FETCH_INIT' });
            try {
                const datas = await query();
                if (!isCancelled) { //isCancelled is true at this point
                    dispatch({ type: 'FETCH_SUCCESS', payload: datas })
                }
            } catch (err) {
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_FAILURE', payload: err })
                }
            }
        })();
        return cancel;
    }, [state.trigger]);
    return { ...state, triggerFetch};
}

用法:

function MyComponent () {
    const { datas, isLoading, isError, triggerFetch } = useFetchApi(query);
    return (
        <form onSubmit={event => {event.preventDefault(); triggerFetch()}}>
...

Tom Finney 的解决方案来自评论:

You could add another use effect that didn't do anything except for return that cancel function and have it with an empty array dependency that would mimic componentWillUnmount like useEffect(() => cancel, [])

const useFetchApi = (query, initialData = []) => {
    let isCancelled = false;
    const [state, dispatch] = useReducer(fetchReducer, {
        isLoading: false,
        isError: false,
        datas: initialData,
        trigger: false
    });
    const triggerFetch = _ => dispatch({ type: 'FETCH_TRIGGER' });
    const cancel = _ => { console.log("canceling");isCancelled = true };

    useEffect(_ => {
        if (!state.trigger)
            return;
        triggerFetch();
        (async _ => {
            dispatch({ type: 'FETCH_INIT' });
            try {
                const datas = await query();
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_SUCCESS', payload: datas })
                }
            } catch (err) {
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_FAILURE', payload: err })
                }
            }
        })();
    }, [state.trigger]);
    useEffect(_=> cancel, []) //remove return cancel from useEffect and replace by this

    return { ...state, triggerFetch};
}

目前,您正在触发状态更改,这将触发重新渲染,从而触发效果,然后调用您的 API。你真正想做的就是打电话给你的 API.

在下面的示例代码中,我更改了 triggerFetch 以实际执行查询并删除了 trigger 状态。我添加了一个没有依赖关系的效果,以允许在卸载时取消。我还更改了取消方法以使用 ref 而不是局部变量,这样它将在重新渲染时持续存在。

import { useReducer, useEffect, useRef } from "react";

const fetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        datas: action.payload
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true
      };
    default:
      throw new Error();
  }
};

const useFetchApi = (query, initialData = []) => {
  const cancelledRef = useRef(false);
  const [state, dispatch] = useReducer(fetchReducer, {
    isLoading: false,
    isError: false,
    datas: initialData,
    trigger: false
  });
  const triggerFetch = async _ => {
    dispatch({ type: "FETCH_INIT" });
    try {
      const datas = await query();
      if (!cancelledRef.current) {
        dispatch({ type: "FETCH_SUCCESS", payload: datas });
      }
    } catch (err) {
      if (!cancelledRef.current) {
        dispatch({ type: "FETCH_FAILURE", payload: err });
      }
    }
  };

  useEffect(_ => {
    return _ => {
      console.log("canceling");
      cancelledRef.current = true;
    };
  }, []);
  return { ...state, triggerFetch };
};

export default useFetchApi;

可以在 useEffect 回调中使用局部变量。感谢@gaearon

https://codesandbox.io/s/k0lm13kwxo

  useEffect(() => {
    let ignore = false;

    async function fetchData() {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
      if (!ignore) setData(result.data);
    }

    fetchData();
    return () => { ignore = true; }
  }, [query]);

2 条注意事项:

  1. clean-up回调运行s每次useEffect运行s; 排除 组件的 mountincludes 组件的 unmount .如果要模拟 componentWillUnMount,请使用具有空依赖项数组的 useEffect。
  2. clean-up 回调 运行s before useEffect 中的其余代码。对于具有依赖关系的 useEffect 来说,记住这一点很重要,因为当任何依赖关系发生变化时都会调用它,并且 clean-up 回调和效果中的其余代码都是 运行。这是 componentDidUpdate 行为。

如果您问我,clean-up 这个名字会让人产生误解。实际上,回调在 componentDidUpdate 场景中的其余代码之前 运行s,因此在这方面它没有准备那么多 clean-up。标题clean-up只有在卸载时才应得。