如何在自定义挂钩中获取数据并将其分派到上下文

How to GET data in a custom hook and dispatch it to context

我对上下文 API 还很陌生,并且对 useState 和 useEffect 之外的钩子有反应,所以请多多包涵。

我正在尝试创建一个自定义 useGet 挂钩,我可以使用它从后端获取一些数据,然后使用上下文 API 存储它,这样如果我在应用程序的其他地方再次使用 Get相同的上下文,它可以首先检查数据是否已被检索并节省一些时间和资源来执行另一个 GET 请求。我正在尝试将其编写为普遍用于各种不同的数据和上下文。

我已经完成了大部分工作,直到我开始尝试将数据分派到 useReducer 状态,然后我收到错误:

Hooks can only be called inside the body of a function component.

我知道我的 dispatch 调用可能违反了钩子规则,但我不明白为什么只有一个调用抛出错误,或者如何修复它以执行我需要的操作。任何帮助将不胜感激。

commandsContext.js

import React, { useReducer, useContext } from "react";

const CommandsState = React.createContext({});
const CommandsDispatch = React.createContext(null);

function CommandsContextProvider({ children }) {
  const [state, dispatch] = useReducer({});
  return (
    <CommandsState.Provider value={state}>
      <CommandsDispatch.Provider value={dispatch}>
        {children}
      </CommandsDispatch.Provider>
    </CommandsState.Provider>
  );
}

function useCommandsState() {
  const context = useContext(CommandsState);
  if (context === undefined) {
    throw new Error("Must be within CommandsState.Provider");
  }
  return context;
}

function useCommandsDispatch() {
  const context = useContext(CommandsDispatch);
  if (context === undefined) {
    throw new Error("Must be within CommandsDispatch.Provider");
  }
  return context;
}

export { CommandsContextProvider, useCommandsState, useCommandsDispatch };

useGet.js

import { API } from "aws-amplify";
import { useRef, useEffect, useReducer } from "react";

export default function useGet(url, useContextState, useContextDispatch) {
  const stateRef = useRef(useContextState);
  const dispatchRef = useRef(useContextDispatch);
  const initialState = {
    status: "idle",
    error: null,
    data: [],
  };

  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case "FETCHING":
        return { ...initialState, status: "fetching" };
      case "FETCHED":
        return { ...initialState, status: "fetched", data: action.payload };
      case "ERROR":
        return { ...initialState, status: "error", error: action.payload };
      default:
        return state;
    }
  }, initialState);

  useEffect(() => {
    if (!url) return;

    const getData = async () => {
      dispatch({ type: "FETCHING" });
      if (stateRef.current[url]) { // < Why doesn't this also cause an error
        const data = stateRef.current[url]; 
        dispatch({ type: "FETCHED", payload: data });
      } else {
        try {
          const response = await API.get("talkbackBE", url);
          dispatchRef.current({ url: response }); // < This causes the error
          dispatch({ type: "FETCHED", payload: response });
        } catch (error) {
          dispatch({ type: "ERROR", payload: error.message });
        }
      }
    };
    getData();
  }, [url]);

  return state;
}

编辑 --

useCommandsState 和 useCommandsDispatch 被导入到我调用 useGet 向下传递的这个组件中。

import {
  useCommandsState,
  useCommandsDispatch,
} from "../../contexts/commandsContext.js";

export default function General({ userId }) {
  const commands = useGet(
    "/commands?userId=" + userId,
    useCommandsState,
    useCommandsDispatch
  );

为什么我只得到 dispatchRef.current 的错误,而不是 stateRef.current,当它们对 useReducer 的 state/dispatch 做完全相同的事情时?

如何重构它来解决我的问题?总而言之,我需要能够在每个上下文的两个或更多位置调用 useGet,第一次调用时将数据存储在传递的上下文中。

这里有各种链接,指向我一直在阅读的内容,这些链接帮助我走到这一步。

How to combine custom hook for data fetching and context?

Updating useReducer 'state' using useEffect

https://reactjs.org/warnings/invalid-hook-call-warning.html

我认为你的问题是因为你使用 useRef 而不是 state 来存储状态。如果您使用 Ref 存储状态,则需要手动告诉 React 更新。

我个人不会使用 reducer,只是坚持使用你熟悉的钩子,因为它们可以满足你当前的需求。我还认为它们是完成这项简单任务的最佳工具,并且更容易遵循。

代码

useGetFromApi.js

这是一个通用且可重复使用的挂钩 - 可以在上下文内外使用

export const useGetFromApi = (url) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!url) return;
    const getData = async () => {      
      try {
        setLoading(true);
        setData(await API.get('talkbackBE', url));
      } catch ({ message }) {
        setError(message);
      } finally {
        setLoading(false); // always set loading to false
      }
    };
    getData();
  }, [url]);

  return { data, error, loading };
};

dataProvider.js

export const DataContext = createContext(null);

export const DataProvider = ({ children, url}) => {
  const { data, error, loading } = useGetFromApi(url);
  return (
    <DataContext.Provider value={{ data, error, loading }}>
      {children}
    </DataContext.Provider>
  );
};

useGet.js

无需检查上下文是否未定义 - React 会让您知道

export const useGet = () => useContext(DataContext);

用法

大多数 parent 需要访问数据的包装组件。此级别 无法 访问数据 - 只有 children 可以!

const PageorLayout = ({children}) => (
 <DataProvider url="">{children}</DataProvider>
)

嵌套在上下文中的页面或组件

const NestedPageorComponent = () => {
  const {data, error, loading } = useGet();
  if(error) return 'error';
  if(loading) return 'loading';
  return <></>;
}

希望这对您有所帮助!

请注意,我在编辑器中的 Stack 上写了大部分内容,因此我无法测试代码,但它应该提供了一个可靠的示例