React hooks&Context:在带有useEffect的子组件中使用上下文时出错

时间:2020-02-07 21:04:30

标签: reactjs react-hooks react-context

我为上下文创建了一个react函数组件,如下所示:

const ItemContext = createContext()

const ItemProvider = (props) => {
    const [item, setItem] = useState(null)

    const findById = (args = {}) => {
        fetch('http://....', { method: 'POST' })
          .then((newItem) => {
            setItem(newItem)
          })
    }

    let value = {
        actions: {
            findById
        },
        state: {
            item
        }
    }

    return <ItemContext.Provider value={value}>
        {props.children}
    </ItemContext.Provider>
}

这样,我的上下文可以处理所有API调用并存储该item的状态。 (类似于redux等)

然后在我的子组件中使用上述上下文的那一行...

const smallComponent = () =>{
    const {id } = useParams()
    const itemContext = useContext(ItemContext)

    useEffect(()=>{
        itemContext.actions.findById(id)
    },[id])

    return <div>info here</div>
}

因此,组件应在id更改时进行API调用。但是我在控制台中收到此错误:

React Hook useEffect缺少依赖项:'itemContext.actions'。要么包含它,要么删除依赖项数组react-hooks / exhaustive-deps

但是,如果我将其添加到依赖项数组中,则会在服务器上出现API调用永无休止的循环。所以我不确定该怎么办。或者,如果我走错路了。谢谢。

===更新==== 这是一个jsfiddle可以尝试:https://jsfiddle.net/zx5t76w2/ (仅供参考,我意识到警告不在控制台中,因为它没有掉毛)

2 个答案:

答案 0 :(得分:2)

您可以仅将useCallback用于获取方法,returns a memoized function:

const findById = useCallback((args = {}) => {
  fetch("http://....", { method: "POST" }).then(newItem => {
    setItem(newItem);
  });
}, []);

...并放在useEffect中:

...
const { actions, state } = useContext(ItemContext)

useEffect(() => {
    actions.findById(id)
}, [id, actions.findById])
...

工作示例:https://jsfiddle.net/6r5jx1h7/1/

您的问题与useEffect一次又一次调用您的自定义钩子有关,因为这是正常的功能,React不会在整个渲染过程中“保存”。


更新

我的最初答案解决了无限循环问题。 您的问题还与您使用上下文的方式有关,因为它一次又一次(See caveats in the official documentation)重新创建上下文的域对象(actionsstate,..)。 / p>

以下是您在Kent C. Dodds'中将上下文划分为statedispatch的绝佳方式的示例,我对此并不推荐。这将修复您的无限循环,并提供上下文使用情况的更清晰的结构。请注意,根据我的原始答案,我仍在使用useCallback作为提取功能:

完整的代码沙箱https://codesandbox.io/s/fancy-sea-bw70b

App.js

import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";

const SmallComponent = () => {
  const id = 5;
  const { username } = useItemState();
  const dispatch = useItemDispatch();

  const fetchUsername = useCallback(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/users/" + id
    );
    const user = await response.json();
    dispatch({ type: "setUsername", usernameUpdated: user.name });
  }, [dispatch]);

  useEffect(() => {
    fetchUsername();
  }, [fetchUsername]);

  return (
    <div>
      <h4>Username from fetch:</h4>
      <p>{username || "not set"}</p>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <ItemProvider>
        <SmallComponent />
      </ItemProvider>
    </div>
  );
}

item-context.js

import React from "react";

const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();

function itemReducer(state, action) {
  switch (action.type) {
    case "setUsername": {
      return { ...state, username: action.usernameUpdated };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function ItemProvider({ children }) {
  const [state, dispatch] = React.useReducer(itemReducer, {
    username: "initial username"
  });
  return (
    <ItemStateContext.Provider value={state}>
      <ItemDispatchContext.Provider value={dispatch}>
        {children}
      </ItemDispatchContext.Provider>
    </ItemStateContext.Provider>
  );
}

function useItemState() {
  const context = React.useContext(ItemStateContext);
  if (context === undefined) {
    throw new Error("useItemState must be used within a CountProvider");
  }
  return context;
}

function useItemDispatch() {
  const context = React.useContext(ItemDispatchContext);
  if (context === undefined) {
    throw new Error("useItemDispatch must be used within a CountProvider");
  }
  return context;
}

export { ItemProvider, useItemState, useItemDispatch };

当我开始使用带有钩子的上下文时,这两个博客文章都对我有很大帮助:

https://kentcdodds.com/blog/application-state-management-with-react https://kentcdodds.com/blog/how-to-use-react-context-effectively

答案 1 :(得分:0)

好吧,我不想写一个答案,因为Bennett基本上给了您解决方法,但是我认为它缺少组件中的该部分,所以您可以开始:

const ItemProvider = ({ children }) => {
   const [item, setItem] = useState(null)

   const findById = useCallback((args = {}) => {
      fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
   }, []);

   return (
      <ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
         {children}
      </ItemContext.Provider>
   )
}

const smallComponent = () => {
   const { id } = useParams()
   const { actions } = useContext(ItemContext)

   useEffect(() => {
     itemContext.actions.findById(id)
   }, [actions.findById, id])

   return <div>info here</div>
}

根据评论内容的扩展,这里是有效的JSFiddle