上下文设置未更新状态

时间:2019-10-31 04:29:21

标签: javascript reactjs typescript react-hooks

上下文总是让我感到困惑。我希望新的Context API会有所帮助,但到目前为止,仍然存在问题。我可以显示初始值,但是基于我的效果的更新不会使用useContext值更新组件。我在做什么错了?

在这里“需要”上下文是过大的选择,但是我想拥有一个能够提供窗口滚动位置值的上下文API,只是为了测试Context Provider / Consumer如何与子代交互:

context.js

import React, { useState, FC, useEffect } from "react";

interface IInitialContext {
  scrollY: number;
  // do to - implement removeEventListener function
}
const intialContext = {
  scrollY: window.scrollY,
  // removeEventListener: () => {},
};

const ScrollContext = React.createContext<IInitialContext>(intialContext);

const VerticalScrollContextProvider: FC = ({ children }) => {
  const [scrollY, setScrollY] = useState(intialContext.scrollY);
  // correctly logs scrollY

  useEffect(() => {
    window.addEventListener(
      "scroll",
      debounce(() => setScrollY(window.scrollY), 100) // correctly captures new scrollY position
    );

    return () => window.removeEventListener("scroll", debounce)
  }, [scrollY]); // re-add listener when scrollY changes

  return (
    <ScrollContext.Provider value={{ scrollY }}> //i don't really need the value passed to component
      {children}
    </ScrollContext.Provider>
  );
};

function useScrollState() {
  const context = React.useContext(ScrollContext);
  if (context === undefined) {
    throw new Error(
      "useScrollState must be used within a VerticalScrollContextProvider"
    );
  }
  return context; // correctly tracks initial state 
}

export { VerticalScrollContextProvider, useScrollState };

和我的组件:

const Home = () => {
  const { scrollY } = useScrollState();
  // correctly has value on app first loading, does not receive updates.
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    console.log("clicked button");
  };
  return (
    <VerticalScrollContextProvider>
      <StyledHome>
        <div id="text-wrapper">
          <StyledFirstName>Phil</StyledFirstName>
          <StyledLastName>Lucks</StyledLastName>
        </div>
        <Button text="clicker" onClick={handleClick} />
      </StyledHome>
    </VerticalScrollContextProvider>
  );
};

export default Home;

我很感谢任何见识。

1 个答案:

答案 0 :(得分:0)

要使上下文正常工作,组件应位于提供程序内部。

来自useContext的文档

  

接受一个上下文对象(从React.createContext返回的值)   并返回该上下文的当前上下文值。目前   上下文值由最近的值prop决定    在树中调用组件上方。

     

当组件上方最近的组件更新时,   这个钩子将通过传递的最新上下文值触发重新渲染   到该MyContext提供程序。

现在密切注意<Home />组件。

const Home = () => {
  const { scrollY } = useScrollState();
  // correctly has value on app first loading, does not receive updates.
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    console.log("clicked button");
  };
  return (
    <VerticalScrollContextProvider>
      <StyledHome>
        <div id="text-wrapper">
          <StyledFirstName>Phil</StyledFirstName>
          <StyledLastName>Lucks</StyledLastName>
        </div>
        <Button text="clicker" onClick={handleClick} />
      </StyledHome>
    </VerticalScrollContextProvider>
  );
};

我们感兴趣的这一行是const { scrollY } = useScrollState();,它试图从父context派生Provider

但是,<Home />组件是否在<Provider />内?

否。 <Provider /><Home />组件内部

运行以下代码片段并查看控制台日志,以查看首先运行哪个组件。

const { useState, useEffect } = React;

const intialContext = {
  scrollY: window.scrollY,
};

const ScrollContext = React.createContext(intialContext);

const VerticalScrollContextProvider = ({ children }) => {
  const [scrollY, setScrollY] = React.useState(intialContext.scrollY);
  // correctly logs scrollY
  console.log("Rendering context");

  useEffect(() => {
    const eventListener = () => setScrollY(window.scrollY);
    window.addEventListener(
      "scroll",
       eventListener// correctly captures new scrollY position
    );

    return () => window.removeEventListener("scroll", eventListener)
  }, [scrollY]); // re-add listener when scrollY changes

  return (
    <ScrollContext.Provider value={{ scrollY }}>
      {children}
    </ScrollContext.Provider>
  );
};

function useScrollState() {
  const context = React.useContext(ScrollContext);
  if (context === undefined) {
    throw new Error(
      "useScrollState must be used within a VerticalScrollContextProvider"
    );
  }
  return context; // correctly tracks initial state 
}





const Home = () => {
  const { scrollY } = useScrollState();
  console.log("Rendering Home");
  // correctly has value on app first loading, does not receive updates.
  const handleClick = (e) => {
    e.preventDefault();
    console.log("clicked button");
  };
  return (
    <VerticalScrollContextProvider>
        <div id="text-wrapper">
          <h3>Phil</h3>
          <h3>Lucks</h3>
        </div>
        <button onClick={handleClick}>Button</button>
        <h2>Scroll and see the console logs</h2>
    </VerticalScrollContextProvider>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));
#root {
height: 1000px;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>

解决方案 ...

Home组件包装在Provider

import React from "react";
import {VerticalScrollContextProvider, useScrollState} from "./context";

const Home = () => {
  const { scrollY } = useScrollState();
  // correctly has value on app first loading, does not receive updates.
  const handleClick = (e) => {
    e.preventDefault();
    console.log("clicked button");
  };
  return (
      <div>
        <div id="text-wrapper">
          <h3>Phil</h3>
          <h3>Lucks</h3>
          {scrollY}
        </div>
        <button text="clicker" onClick={handleClick} />
      </div>
  );
};

export default () => (
  <VerticalScrollContextProvider><Home /></VerticalScrollContextProvider>
);

运行下面的代码片段并滚动以查看控制台日志。

const {
  useState,
  useEffect
} = React;

const intialContext = {
  scrollY: window.scrollY,
};

const ScrollContext = React.createContext(intialContext);

const VerticalScrollContextProvider = ({
  children
}) => {
  const [scrollY, setScrollY] = React.useState(intialContext.scrollY);
  // correctly logs scrollY
  console.log("Rendering context");

  useEffect(() => {
    const eventListener = () => setScrollY(window.scrollY);
    window.addEventListener(
      "scroll",
      eventListener // correctly captures new scrollY position
    );

    return () => window.removeEventListener("scroll", eventListener)
  }, [scrollY]); // re-add listener when scrollY changes

  return ( <
    ScrollContext.Provider value = {
      {
        scrollY
      }
    } > {
      children
    } <
    /ScrollContext.Provider>
  );
};

function useScrollState() {
  const context = React.useContext(ScrollContext);
  if (context === undefined) {
    throw new Error(
      "useScrollState must be used within a VerticalScrollContextProvider"
    );
  }
  return context; // correctly tracks initial state 
}





const Home = () => {
  const {
    scrollY
  } = useScrollState();
  console.log("Rendering Home", scrollY);
  // correctly has value on app first loading, does not receive updates.
  const handleClick = (e) => {
    e.preventDefault();
    console.log("clicked button");
  };
  return ( <
    React.Fragment >
    <
    div id = "text-wrapper" >
    <
    h3 > Phil < /h3> <
    h3 > Lucks < /h3> <
    /div> <
    button onClick = {
      handleClick
    } > Button < /button> <
    h2 > Scroll and see the console logs < /h2> <
    /React.Fragment>
  );
};

const HomeWithProvider = () => {
  return <VerticalScrollContextProvider >
    <
    Home / >
    <
    /VerticalScrollContextProvider>
}

ReactDOM.render( < HomeWithProvider / > , document.getElementById("root"));
#root {
  height: 1000px;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>