首页 > 解决方案 > 反应上下文状态值未在消费者中更新

问题描述

通过“调度程序”设置为“搜索词”的第一个值在任何后续调用后仍然存在,我试图弄清楚为什么会这样或错误在哪里。

我已经<ContextProvider />定义了“搜索词”的状态,并且“搜索词”的值可能会因由“调度程序”触发的事件<ContextConsumer />或嵌套组件而改变。<ContextConsumer />我发现在调用“reducer”之后没有找到所需的状态,即使考虑到“状态”的变化不是立即的。

为简洁起见,下面发布的组件或代码已被简化以隔离主题,因此可能存在一些拼写错误,例如未声明的变量(因为我已经删除了不相关的代码块)。

上下文提供程序看起来像:

import React from 'react'

export const POSTS_SEARCH_RESULTS = 'POSTS_SEARCH_RESULTS'

export const GlobalStateContext = React.createContext()
export const GlobalDispatchContext = React.createContext()

const initialState = {
  posts: [],
  searchTerm: ''
}

const reducer = (state, action) => {
  switch (action.type) {
    case POSTS_SEARCH_RESULTS: {
      return {
        ...state,
        posts: action.posts,
        searchTerm: action.searchTerm
      }
    }

    default:
      throw new Error('Bad Action Type')
  }
}

const GlobalContextProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  return (
    <GlobalStateContext.Provider value={state}>
      <GlobalDispatchContext.Provider value={dispatch}>
        {children}
      </GlobalDispatchContext.Provider>
    </GlobalStateContext.Provider>
  )
}

export default GlobalContextProvider

消费者看起来像:

const Search = () => {
  const state = useContext(GlobalStateContext)
  const { searchTerm, posts } = state

  useEffect(() => {
    console.log('[debug] <Search />: searchTerm: ', searchTerm);
  }, [searchTerm])

  return (  
     <>
       <LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
     </>

  )
}

export default Search

接下来是嵌套的 Consumer Children 组件。useEffect具有对searchTerm;的依赖项 这个值是通过“dispatcher”设置的,并通过 Consumer 中的 useContentxt 获取。

dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })

并像这样消费:

  const state = useContext(GlobalStateContext)
  const { searchTerm, posts } = state

并传递给,例如<LoadMoreScroll searchTerm={searchTerm} />

所以,我所拥有但失败的是:

const LoadMoreScroll = ({ searchTerm, posts, postCursor }) => {
  const dispatch = useContext(GlobalDispatchContext)
  const [postsCached, setPostsCached] = useState(posts)
  const [loading, setLoading] = useState(false)
  const refScroll = useRef(null)
  const [first] = useState(POSTS_SEARCH_INITIAL_NUMBER)
  const [after, setAfter] = useState(postCursor)
  const [isVisible, setIsVisible] = useState(false)
  const [term, setTerm] = useState(searchTerm)

  useEffect(() => {
    loadMore({ first, after, term })
  }, [isVisible])

  useEffect(() => {
    dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm })
  }, [postsCached])

  useEffect(() => {
    setTerm(searchTerm)
    const handler = _debounce(handleScroll, 1200)
    window.addEventListener('scroll', handler)
    return () => window.removeEventListener('scroll', handler)
  }, [searchTerm])

  const handleScroll = () => {
    const offset = -(window.innerHeight * 0.1)
    const top = refScroll.current.getBoundingClientRect().top
    const isVisible = (top + offset) >= 0 && (top - offset) <= window.innerHeight
    isVisible && setIsVisible(true)
  }

  const loadMore = async ({ first, after, term }) => {
    if (loading) return
    setLoading(true)

    const result = await searchFor({
      first,
      after,
      term
    })

    const nextPosts = result.data

    setPostsCached([...postsCached, ...nextPosts])
    setAfter(postCursor)
    setLoading(false)
    setIsVisible(false)
  }

  return (
    <div ref={refScroll} className={style.loaderContainer}>
      { loading && <Loader /> }
    </div>
  )
}

export default LoadMoreScroll

预期的结果是让<LoadMoreScroll />“dispatcher”分配的“searchTerm”的最新值传递给“loadMore”函数,但失败了。相反,它使用从第一次调用“调度程序”开始的“初始值”。这是在对“dispatcher”的初始调用之后的任何后续“dispatcher”调用:

dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })

那应该更新上下文“searchTerm”,但没有做到。在上面的源代码中,loadmore 保存了设置的初始值!

单独的示例具有类似的逻辑,可以正常工作(https://codesandbox.io/s/trusting-booth-1w40e?fontsize=14&hidenavigation=1&theme=dark

希望尽快用解决方案更新上述问题,如果有人发现问题,请告诉我!

标签: reactjsreact-hooksreact-context

解决方案


代码沙盒链接有效,但在创建和使用context.

在提供的代码中,您创建了两个单独的提供程序。一个具有 state 的值,一个具有 dispatch 的值。

  <GlobalStateContext.Provider value={state}>
      <GlobalDispatchContext.Provider value={dispatch}>

但是,codesandbox 使用两者statedispatch在同一个provider.

 <Application.Provider value={{ state, dispatch }}>

而且它似乎GlobalContextProvider是出口的,但我不确定它是否用于包装任何消费者。

由于 和 是分开的dispatchstate我将把它用于我提出的解决方案。

该实现似乎是正确的,但在我看来,您可以更进一步并创建两个自定义钩子,它们仅公开一种提供上下文值的方式和一种使用它的方式。

import React from "react";

export const POSTS_SEARCH_RESULTS = "POSTS_SEARCH_RESULTS";

// 
// notice that we don't need to export these anymore as we are going to be 
//
// using them in our custom hooks useGlobalState and useGlobalDispatch
//
//
const GlobalStateContext = React.createContext();
const GlobalDispatchContext = React.createContext();

const initialState = {
  posts: [],
  searchTerm: "",
};

const reducer = (state, action) => {
  switch (action.type) {
    case POSTS_SEARCH_RESULTS: {
      return {
        ...state,
        posts: action.posts,
        searchTerm: action.searchTerm
      };
    }

    default:
      throw new Error("Bad Action Type");
  }
};

const GlobalContextProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <GlobalStateContext.Provider value={state}>
      <GlobalDispatchContext.Provider value={dispatch}>
        {children}
      </GlobalDispatchContext.Provider>
    </GlobalStateContext.Provider>
  );
};


// If any of these hooks is not being called within a function component 
// that is rendered within the `GlobalContextProvider`, 
// we throw an error

const useGlobalState = () => {
  const context = React.useContext(GlobalStateContext);
  if (context === undefined) {
    throw new Error(
      "useGlobalState must be used within a GlobalContextProvider"
    );
  }
  return context;
};

const useGlobalDispatch = () => {
  const context = React.useContext(GlobalDispatchContext);
  if (context === undefined) {
    throw new Error(
      "useGlobalDispatch must be used within a GlobalContextProvider"
    );
  }
  return context;
};

// We only export the custom hooks for state and dispatch 
// and of course our`GlobalContextProvider`, which we are 
// going to wrap any part of our app that 
// needs to make use of this state

export { GlobalContextProvider, useGlobalState, useGlobalDispatch };

我在这里添加的只是几个自定义钩子,它们公开了每个上下文,即GlobalStateContext并将GlobalDispatchContext它们与GlobalContextProvider.

如果我们想让它在整个应用程序中全局可用,我们可以将组件包裹GlobalContextProvider起来App

function App() {
  return (
    <div className="App">
      <Search />
    </div>
  );
}

// If you forget to wrap the consumer with your provider, the custom hook will 
// throw an error letting you know that the hook is not being called 
// within a function component that is rendered within the 
// GlobalContextProvider as it's supposed to

const AppContainer = () => (
  <GlobalContextProvider>
    <App />
  </GlobalContextProvider>
);

export default AppContainer;

如果您想state在应用程序的任何部分或dispatch任何操作中使用 ,则需要导入之前创建的相关自定义挂钩。

在您的搜索组件中,这将类似于以下示例:

import { useGlobalState, useGlobalDispatch } from "./Store";

const Search = () => {

  // Since we are doing this in our custom hook that is not needed anymore
  // const state = useContext(GlobalStateContext)
  // if you need to dispatch any actions you can 
  // import the useGlobalDispatch hook and use it like so: 
  // const dispatch = useGlobalDispatch();


   const state = useGlobalState(); 
   const { searchTerm, posts } = state

  useEffect(() => {
    console.log('[debug] <Search />: searchTerm: ', searchTerm);
  }, [searchTerm])

  return (  
     <>
       <LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
     </>

  )
}

export default Search

由于问题中提供的代码框缺少一些部分,因此我已将其重构为此概念的简化工作版本希望能帮助解决您的问题。

当我遇到 Context API 和钩子问题时,我还发现这篇文章很有帮助。

它遵循相同的模式,我一直在生产中使用它并且对结果非常满意。

希望有帮助:)


推荐阅读