Реагирует на неожиданно вызванную функцию очистки useEffect - PullRequest
3 голосов
/ 27 марта 2019

Я создаю пользовательский хук для извлечения api при отправке формы, я делаю вызов api внутри хука useEffect и у меня есть редуктор для обработки состояний хука. Одно из состояний trigger вначале установлено в false, что контролирует, если useEffect что-то делает, точка в том, что ловушка возвращает функцию, которая переворачивает значение trigger, которое вызывает useEffect, только когда вы вызываете эту функцию. Проблема заключается в том, что функция очистки useEffect вызывается во время вызова API, даже если компонент все еще монтируется.

Функция очистки, похоже, сработала, потому что я установил значение 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()}}>
...

Ответы [ 3 ]

2 голосов
/ 14 мая 2019

Локальная переменная может использоваться внутри обратного вызова 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]);
0 голосов
/ 27 марта 2019

В настоящее время вы запускаете изменение состояния, которое запускает повторную визуализацию, которая вызывает эффект, который затем вызывает ваш API. Все, что вы действительно хотите сделать, это вызвать свой API.

В приведенном ниже примере кода я изменил triggerFetch, чтобы фактически выполнить запрос, и удалил состояние trigger. Я добавил эффект без зависимостей, чтобы разрешить отмену при размонтировании. Я также изменил подход отмены, чтобы использовать ссылку, а не локальную переменную, чтобы она сохранялась при повторном рендеринге.

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;
0 голосов
/ 27 марта 2019

Решение от Тома Финни из комментариев:

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};
}
...