【问题标题】:How to GET data in a custom hook and dispatch it to context如何在自定义钩子中获取数据并将其分派到上下文
【发布时间】:2021-09-24 15:39:06
【问题描述】:

我对上下文 API 还很陌生,并且对 useState 和 useEffect 之外的钩子做出反应,所以请与我交流。

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

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

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

我知道我的调度调用可能违反了挂钩规则,但我不明白为什么只有一个调用会引发错误,或者如何修复它以完成我需要的操作。任何帮助将不胜感激。

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

使用Get.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

Accessing context from useEffect

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

【问题讨论】:

  • 它们作为 useContextState 和 useContextDispatch 传递给 useGet。像这样命名以便它们可以被普遍使用,它只是在这种情况下被用于 CommandsContext
  • 我已将其编辑到我的问题中。
  • 我需要在不同嵌套级别的大约 3 个组件之间共享状态,并且不想为每个组件执行 GET 请求。

标签: reactjs react-hooks react-context


【解决方案1】:

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

我个人不会使用 reducer,而是坚持使用您熟悉的钩子,因为它们可以满足您当前的要求。我也认为它们是完成这项简单任务的最佳工具,并且更容易理解。

代码

使用GetFromApi.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>
  );
};

使用Get.js

不需要检查上下文是否未定义 - React 会让你知道

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

用法

大多数需要访问数据的父包装组件。此级别有权访问数据 - 只有其子级才能访问!

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

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

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

希望这会有所帮助!

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

【讨论】:

  • 谢谢,这绝对有帮助。我唯一的问题是我不知道首先要加载哪个组件。我想我可以在 App 级别使用GetFromAPI,然后在所有较低的组件上使用Get。
  • 如果你只是先调用 useGetFromAPI - 你就违背了上下文的目的。您可以强制子组件在上下文中的查询完成之前不呈现,或者只检查数据是否已加载并显示加载组件,直到查询完成。
  • 啊,我想我现在明白它是如何工作的了。 GetFromAPI 在提供程序中。
  • useGetFromAPI 有意与上下文文件分离,以使代码更易于阅读并使其可重用。您可以将整个钩子放在您的上下文中 - 您只会失去可重用性。
  • 很高兴它成功了。减速器有它们的位置,我认为在这种情况下你不需要它。您可以向数据提供者添加一个函数,可以调用该函数来更新并将其公开给您的上下文。
猜你喜欢
  • 1970-01-01
  • 2022-01-07
  • 1970-01-01
  • 1970-01-01
  • 2020-11-07
  • 2022-10-08
  • 2021-04-28
  • 2021-08-21
  • 2020-04-21
相关资源
最近更新 更多