Чтобы контекст работал, компонент должен находиться внутри провайдера.
Из документов 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 />
Component
Запустите приведенный ниже фрагмент кода и просмотрите журнал консоли, чтобы увидеть, какой компонент запускается первым.
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>