React Hooks - 上下文提供者没有立即反映调度状态
React Hooks - Context Provider is not immediately reflecting dispatched state
我的目标:
我正在创建一个向用户显示数据的 table。基于存储在全局状态中的某个值(存储在上下文 API 提供给其他组件的 reducer 函数中),我将 header 固定到滚动页面的顶部,但是 仅 当 table 在视图中时 。为此,我必须注册一个 on Scroll 和 on Resize 事件侦听器,以便在用户滚动或调整屏幕大小时重新计算 tables 的位置。我想更新全局状态为 isHeaderActivelyFixed: true
仅当 table 是 并且状态 尚未 设置为 isHeaderActivelyFixed: true
。否则,每次 table 在视图中并且用户滚动到 isHeaderActivelyFixed: true
时我会不断更新状态,同样当它不在 isHeaderActivelyFixed: false
视图中时
问题:
我按照我认为需要的方式设置了上述场景。但是,当我发送到全局状态然后控制台日志或使用该全局状态时,它并没有反映我刚刚发送给它的内容。 React 开发工具 DO 显示我发送的更新状态,但我需要能够在我发送它的函数中更新新发送的状态。这样我就知道不再发送它.我希望这是有道理的。提前致谢!
代码:(注意:我删除了不必要的代码,所以有些东西看起来很奇怪。我留下了一些代码来提供问题的上下文。我评论的是问题出现的地方。isActivelyViewed()
函数只是获取 tables getBoundingClientRect()
并检查它是否仍在视图中)
ProductTableStore.jsx
import React from 'react';
const initialState = {
isLoading: true,
isSelectable: null,
isHeaderFixedOnScroll: null,
isHeaderActivelyFixed: null,
isAddToCartEnabled: null,
productTableActiveWidth: null,
addToCartData: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setIsHeaderFixedOnScroll':
return {
...state,
isHeaderFixedOnScroll: action.isHeaderFixedOnScroll,
};
case 'setIsHeaderActivelyFixed':
return {
...state,
isHeaderActivelyFixed: action.isHeaderActivelyFixed,
};
case 'setProductTableActiveWidth':
return {
...state,
productTableActiveWidth: action.productTableActiveWidth,
};
default:
throw new Error(
`Unexpected or missing action type. Action type provided was: ${action.type}`
);
}
};
const ProductTableContext = React.createContext({});
const ProductTableStore = () => {
return React.useContext(ProductTableContext);
};
const ProductTableProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<ProductTableContext.Provider value={[state, dispatch]}>
{children}
</ProductTableContext.Provider>
);
};
export { ProductTableStore, ProductTableProvider };
ProductTable.jsx(我有问题的文件)
import React from 'react';
import { ProductTableStore } from './ProductTableStore/ProductTableStore';
import { isActivelyViewed } from '../../js/helpers';
const ProductTable = ({ heading, ariaLabel, children }) => {
const [globalState, dispatch] = ProductTableStore();
const [isOnScrollResizeEventRegistered, setIsOnScrollResizeEventRegistered] = React.useState(
null
);
const ProductTableRef = React.useRef(null);
const registerOnScrollResizeEvent = (ref, resolve) => {
console.log('Registering onScrollandResize');
window.addEventListener(
'scroll',
_.throttle(() => {
calculatePosition(ref);
}),
10
);
window.addEventListener(
'resize',
_.throttle(() => {
calculateProductTableValues(ref);
}),
10
);
if (resolve) resolve();
};
const setIsHeaderActivelyFixed = (isHeaderActivelyFixed) => {
console.log('fx setIsHeaderActivelyFixed. Passed argument:', isHeaderActivelyFixed);
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed,
});
console.log('isHeaderActivelyFixed', globalState.isHeaderActivelyFixed);
// This comes back null and idk why! I thought it may be because there hasn't been a re-render but
// changing the callback on the effect below doesn't seem to change that
};
const setProductTableActiveWidth = (productTableActiveWidth) => {
console.log('fx setProductTableActiveWidth');
dispatch({
type: 'setProductTableActiveWidth',
productTableActiveWidth: `${productTableActiveWidth}px`,
});
console.log('productTableActiveWidth', globalState.productTableActiveWidth);
// This comes back null and idk why! I thought it may be because there hasn't been a re-render but
// changing the callback on the effect below doesn't seem to change that
};
const calculatePosition = (ref) => {
if (isActivelyViewed(ref.current) && !globalState.isHeaderActivelyFixed) {
setIsHeaderActivelyFixed(true);
} else if (!isActivelyViewed(ref.current) && globalState.isHeaderActivelyFixed) {
// This never works because globalState never reflects updates in this function
setIsHeaderActivelyFixed(false);
} else {
console.log('None of these...');
}
};
const calculateWidth = (ref) => {
if (ref.current.offsetWidth !== globalState.productTableActiveWidth) {
setProductTableActiveWidth(ref.current.offsetWidth);
}
};
const calculateProductTableValues = (ProductTableRef, resolve) => {
calculateWidth(ProductTableRef);
calculatePosition(ProductTableRef);
if (resolve) resolve();
};
React.useEffect(() => {
if (!globalState.isHeaderFixedOnScroll) return;
new Promise((resolve, reject) => {
if (isOnScrollResizeEventRegistered) reject();
if (!isOnScrollResizeEventRegistered) {
// Calculate intital PT width so that we only have to recalculate on resize
calculateProductTableValues(ProductTableRef, resolve);
}
})
.then(() => {
registerOnScrollResizeEvent(ProductTableRef);
})
.then(() => {
setIsOnScrollResizeEventRegistered(true);
})
.catch((err) => {
console.error(
'Unable to create promise for fixing the Product Table Header on scroll. The error returned was: ',
err
);
});
}, [globalState.isHeaderFixedOnScroll]);
return (
<ThemeProvider theme={getSiteTheme(_app.i18n.getString({ code: 'styledComponents.theme' }))}>
<StyledProductTableContainer>
{globalState.isAddToCartEnabled && (
<StyledAddToCartContainer>
<AddToCartForm
buttonText={_app.i18n.getString({ code: 'cart.add.allItems' })}
isDisabled={globalState.addToCartData.length === 0}
ajaxData={globalState.addToCartData}
/>
</StyledAddToCartContainer>
)}
{heading && <FeaturedHeader>{heading}</FeaturedHeader>}
<StyledProductTable ref={ProductTableRef} ariaLabel={ariaLabel}>
{globalState.isLoading && (
<ThemeProvider theme={loadingStyles}>
<StyledLoadingSpinner />
</ThemeProvider>
)}
{children}
</StyledProductTable>
</StyledProductTableContainer>
</ThemeProvider>
);
};
const ProductTableHeader = ({ children }) => {
const [globalState] = ProductTableStore();
return (
<StyledProductTableHeader
isSelectable={globalState.isSelectable}
isHeaderFixedOnScroll={globalState.isHeaderFixedOnScroll}
isHeaderActivelyFixed={globalState.isHeaderActivelyFixed}
fixedWidth={globalState.productTableActiveWidth}
>
{globalState.isSelectable && (
<StyledProductTableLabel isSelect>Select</StyledProductTableLabel>
)}
{children}
</StyledProductTableHeader>
);
};
const ProductTableRow = ({ children }) => {
const [globalState] = ProductTableStore();
return (
<StyledProductTableRow isSelectable={globalState.isSelectable}>
{globalState.isSelectable && (
<StyledProductTableCell isSelect>
<GenericCheckbox />
</StyledProductTableCell>
)}
{children}
</StyledProductTableRow>
);
};
export {
ProductTable,
ProductTableHeader,
ProductTableRow,
};
试试这个
const setIsHeaderActivelyFixed = (isHeader) => {
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed: isHeader
});
};
解决方案
我最终创建了一个自定义挂钩来处理这个问题。核心问题是我试图依赖未立即更新的全局状态值。相反,我创建了与我分配给全局状态的值相匹配的引用,并改为检查引用。
ProductTableStore.jsx(全局状态文件)
从 'react';
导入 React
const initialState = {
isLoading: true,
isSelectable: null,
isHeaderFixedOnScroll: null,
isHeaderActivelyFixed: null,
isAddToCartEnabled: null,
productTableActiveWidth: null,
addToCartData: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setIsHeaderFixedOnScroll':
return {
...state,
isHeaderFixedOnScroll: action.isHeaderFixedOnScroll,
};
case 'setIsHeaderActivelyFixed':
return {
...state,
isHeaderActivelyFixed: action.isHeaderActivelyFixed,
};
case 'setProductTableActiveWidth':
return {
...state,
productTableActiveWidth: action.productTableActiveWidth,
};
default:
throw new Error(
`Unexpected or missing action type. Action type provided was: ${action.type}`
);
}
};
const ProductTableContext = React.createContext({});
const ProductTableStore = () => {
return React.useContext(ProductTableContext);
};
const ProductTableProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<ProductTableContext.Provider value={[state, dispatch]}>
{children}
</ProductTableContext.Provider>
);
};
export { ProductTableStore, ProductTableProvider };
ProductTable.jsx
const ProductTable = ({ heading, ariaLabel, children }) => {
const [globalState, dispatch] = ProductTableStore();
const ProductTableRef = React.useRef(null);
const setIsHeaderActivelyFixed = (isHeaderActivelyFixed) => {
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed,
});
};
const setProductTableActiveWidth = (productTableActiveWidth) => {
dispatch({
type: 'setProductTableActiveWidth',
productTableActiveWidth: `${productTableActiveWidth}px`,
});
};
const useShouldHeaderBeFixed = (ref) => {
if (!globalState.isHeaderFixedOnScroll) return;
// keep mutable refs of values pertinent to the fixed header for the lifetime of the component
const fixedState = React.useRef(null);
const fixedWidth = React.useRef(null);
const [shouldHeaderBeFixed, setShouldHeaderBeFixed] = React.useState(false);
const calculateTablePosition = () => {
if (!fixedState.current && isActivelyViewed(ref.current)) {
setShouldHeaderBeFixed(true);
fixedState.current = true;
} else if (!!fixedState.current && !isActivelyViewed(ref.current)) {
setShouldHeaderBeFixed(false);
fixedState.current = false;
}
};
const calculateTableWidth = () => {
if (fixedWidth.current !== ProductTableRef.current.offsetWidth) {
setProductTableActiveWidth(ProductTableRef.current.offsetWidth);
fixedWidth.current = ProductTableRef.current.offsetWidth;
}
};
const calculateTablePositionAndWidth = () => {
calculateTablePosition();
calculateTableWidth();
};
React.useEffect(() => {
calculateTablePositionAndWidth();
}, []);
React.useEffect(() => {
window.addEventListener('scroll', calculateTablePosition);
window.addEventListener('resize', calculateTablePositionAndWidth);
return () => {
window.removeEventListener('scroll', calculateTablePosition);
window.removeEventListener('resize', calculateTablePositionAndWidth);
};
}, [isActivelyViewed(ref.current)]);
return shouldHeaderBeFixed;
};
// initiallize our custom hook
const shouldHeaderBeFixed = useShouldHeaderBeFixed(ProductTableRef);
// listen only to our custom hook to set global state for the fixed header
React.useEffect(() => {
setIsHeaderActivelyFixed(shouldHeaderBeFixed);
}, [shouldHeaderBeFixed, globalState.isHeaderFixedOnScroll]);
...
我的目标:
我正在创建一个向用户显示数据的 table。基于存储在全局状态中的某个值(存储在上下文 API 提供给其他组件的 reducer 函数中),我将 header 固定到滚动页面的顶部,但是 仅 当 table 在视图中时 。为此,我必须注册一个 on Scroll 和 on Resize 事件侦听器,以便在用户滚动或调整屏幕大小时重新计算 tables 的位置。我想更新全局状态为 isHeaderActivelyFixed: true
仅当 table 是 并且状态 尚未 设置为 isHeaderActivelyFixed: true
。否则,每次 table 在视图中并且用户滚动到 isHeaderActivelyFixed: true
时我会不断更新状态,同样当它不在 isHeaderActivelyFixed: false
问题: 我按照我认为需要的方式设置了上述场景。但是,当我发送到全局状态然后控制台日志或使用该全局状态时,它并没有反映我刚刚发送给它的内容。 React 开发工具 DO 显示我发送的更新状态,但我需要能够在我发送它的函数中更新新发送的状态。这样我就知道不再发送它.我希望这是有道理的。提前致谢!
代码:(注意:我删除了不必要的代码,所以有些东西看起来很奇怪。我留下了一些代码来提供问题的上下文。我评论的是问题出现的地方。isActivelyViewed()
函数只是获取 tables getBoundingClientRect()
并检查它是否仍在视图中)
ProductTableStore.jsx
import React from 'react';
const initialState = {
isLoading: true,
isSelectable: null,
isHeaderFixedOnScroll: null,
isHeaderActivelyFixed: null,
isAddToCartEnabled: null,
productTableActiveWidth: null,
addToCartData: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setIsHeaderFixedOnScroll':
return {
...state,
isHeaderFixedOnScroll: action.isHeaderFixedOnScroll,
};
case 'setIsHeaderActivelyFixed':
return {
...state,
isHeaderActivelyFixed: action.isHeaderActivelyFixed,
};
case 'setProductTableActiveWidth':
return {
...state,
productTableActiveWidth: action.productTableActiveWidth,
};
default:
throw new Error(
`Unexpected or missing action type. Action type provided was: ${action.type}`
);
}
};
const ProductTableContext = React.createContext({});
const ProductTableStore = () => {
return React.useContext(ProductTableContext);
};
const ProductTableProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<ProductTableContext.Provider value={[state, dispatch]}>
{children}
</ProductTableContext.Provider>
);
};
export { ProductTableStore, ProductTableProvider };
ProductTable.jsx(我有问题的文件)
import React from 'react';
import { ProductTableStore } from './ProductTableStore/ProductTableStore';
import { isActivelyViewed } from '../../js/helpers';
const ProductTable = ({ heading, ariaLabel, children }) => {
const [globalState, dispatch] = ProductTableStore();
const [isOnScrollResizeEventRegistered, setIsOnScrollResizeEventRegistered] = React.useState(
null
);
const ProductTableRef = React.useRef(null);
const registerOnScrollResizeEvent = (ref, resolve) => {
console.log('Registering onScrollandResize');
window.addEventListener(
'scroll',
_.throttle(() => {
calculatePosition(ref);
}),
10
);
window.addEventListener(
'resize',
_.throttle(() => {
calculateProductTableValues(ref);
}),
10
);
if (resolve) resolve();
};
const setIsHeaderActivelyFixed = (isHeaderActivelyFixed) => {
console.log('fx setIsHeaderActivelyFixed. Passed argument:', isHeaderActivelyFixed);
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed,
});
console.log('isHeaderActivelyFixed', globalState.isHeaderActivelyFixed);
// This comes back null and idk why! I thought it may be because there hasn't been a re-render but
// changing the callback on the effect below doesn't seem to change that
};
const setProductTableActiveWidth = (productTableActiveWidth) => {
console.log('fx setProductTableActiveWidth');
dispatch({
type: 'setProductTableActiveWidth',
productTableActiveWidth: `${productTableActiveWidth}px`,
});
console.log('productTableActiveWidth', globalState.productTableActiveWidth);
// This comes back null and idk why! I thought it may be because there hasn't been a re-render but
// changing the callback on the effect below doesn't seem to change that
};
const calculatePosition = (ref) => {
if (isActivelyViewed(ref.current) && !globalState.isHeaderActivelyFixed) {
setIsHeaderActivelyFixed(true);
} else if (!isActivelyViewed(ref.current) && globalState.isHeaderActivelyFixed) {
// This never works because globalState never reflects updates in this function
setIsHeaderActivelyFixed(false);
} else {
console.log('None of these...');
}
};
const calculateWidth = (ref) => {
if (ref.current.offsetWidth !== globalState.productTableActiveWidth) {
setProductTableActiveWidth(ref.current.offsetWidth);
}
};
const calculateProductTableValues = (ProductTableRef, resolve) => {
calculateWidth(ProductTableRef);
calculatePosition(ProductTableRef);
if (resolve) resolve();
};
React.useEffect(() => {
if (!globalState.isHeaderFixedOnScroll) return;
new Promise((resolve, reject) => {
if (isOnScrollResizeEventRegistered) reject();
if (!isOnScrollResizeEventRegistered) {
// Calculate intital PT width so that we only have to recalculate on resize
calculateProductTableValues(ProductTableRef, resolve);
}
})
.then(() => {
registerOnScrollResizeEvent(ProductTableRef);
})
.then(() => {
setIsOnScrollResizeEventRegistered(true);
})
.catch((err) => {
console.error(
'Unable to create promise for fixing the Product Table Header on scroll. The error returned was: ',
err
);
});
}, [globalState.isHeaderFixedOnScroll]);
return (
<ThemeProvider theme={getSiteTheme(_app.i18n.getString({ code: 'styledComponents.theme' }))}>
<StyledProductTableContainer>
{globalState.isAddToCartEnabled && (
<StyledAddToCartContainer>
<AddToCartForm
buttonText={_app.i18n.getString({ code: 'cart.add.allItems' })}
isDisabled={globalState.addToCartData.length === 0}
ajaxData={globalState.addToCartData}
/>
</StyledAddToCartContainer>
)}
{heading && <FeaturedHeader>{heading}</FeaturedHeader>}
<StyledProductTable ref={ProductTableRef} ariaLabel={ariaLabel}>
{globalState.isLoading && (
<ThemeProvider theme={loadingStyles}>
<StyledLoadingSpinner />
</ThemeProvider>
)}
{children}
</StyledProductTable>
</StyledProductTableContainer>
</ThemeProvider>
);
};
const ProductTableHeader = ({ children }) => {
const [globalState] = ProductTableStore();
return (
<StyledProductTableHeader
isSelectable={globalState.isSelectable}
isHeaderFixedOnScroll={globalState.isHeaderFixedOnScroll}
isHeaderActivelyFixed={globalState.isHeaderActivelyFixed}
fixedWidth={globalState.productTableActiveWidth}
>
{globalState.isSelectable && (
<StyledProductTableLabel isSelect>Select</StyledProductTableLabel>
)}
{children}
</StyledProductTableHeader>
);
};
const ProductTableRow = ({ children }) => {
const [globalState] = ProductTableStore();
return (
<StyledProductTableRow isSelectable={globalState.isSelectable}>
{globalState.isSelectable && (
<StyledProductTableCell isSelect>
<GenericCheckbox />
</StyledProductTableCell>
)}
{children}
</StyledProductTableRow>
);
};
export {
ProductTable,
ProductTableHeader,
ProductTableRow,
};
试试这个
const setIsHeaderActivelyFixed = (isHeader) => {
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed: isHeader
});
};
解决方案
我最终创建了一个自定义挂钩来处理这个问题。核心问题是我试图依赖未立即更新的全局状态值。相反,我创建了与我分配给全局状态的值相匹配的引用,并改为检查引用。
ProductTableStore.jsx(全局状态文件) 从 'react';
导入 Reactconst initialState = {
isLoading: true,
isSelectable: null,
isHeaderFixedOnScroll: null,
isHeaderActivelyFixed: null,
isAddToCartEnabled: null,
productTableActiveWidth: null,
addToCartData: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setIsHeaderFixedOnScroll':
return {
...state,
isHeaderFixedOnScroll: action.isHeaderFixedOnScroll,
};
case 'setIsHeaderActivelyFixed':
return {
...state,
isHeaderActivelyFixed: action.isHeaderActivelyFixed,
};
case 'setProductTableActiveWidth':
return {
...state,
productTableActiveWidth: action.productTableActiveWidth,
};
default:
throw new Error(
`Unexpected or missing action type. Action type provided was: ${action.type}`
);
}
};
const ProductTableContext = React.createContext({});
const ProductTableStore = () => {
return React.useContext(ProductTableContext);
};
const ProductTableProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<ProductTableContext.Provider value={[state, dispatch]}>
{children}
</ProductTableContext.Provider>
);
};
export { ProductTableStore, ProductTableProvider };
ProductTable.jsx
const ProductTable = ({ heading, ariaLabel, children }) => {
const [globalState, dispatch] = ProductTableStore();
const ProductTableRef = React.useRef(null);
const setIsHeaderActivelyFixed = (isHeaderActivelyFixed) => {
dispatch({
type: 'setIsHeaderActivelyFixed',
isHeaderActivelyFixed,
});
};
const setProductTableActiveWidth = (productTableActiveWidth) => {
dispatch({
type: 'setProductTableActiveWidth',
productTableActiveWidth: `${productTableActiveWidth}px`,
});
};
const useShouldHeaderBeFixed = (ref) => {
if (!globalState.isHeaderFixedOnScroll) return;
// keep mutable refs of values pertinent to the fixed header for the lifetime of the component
const fixedState = React.useRef(null);
const fixedWidth = React.useRef(null);
const [shouldHeaderBeFixed, setShouldHeaderBeFixed] = React.useState(false);
const calculateTablePosition = () => {
if (!fixedState.current && isActivelyViewed(ref.current)) {
setShouldHeaderBeFixed(true);
fixedState.current = true;
} else if (!!fixedState.current && !isActivelyViewed(ref.current)) {
setShouldHeaderBeFixed(false);
fixedState.current = false;
}
};
const calculateTableWidth = () => {
if (fixedWidth.current !== ProductTableRef.current.offsetWidth) {
setProductTableActiveWidth(ProductTableRef.current.offsetWidth);
fixedWidth.current = ProductTableRef.current.offsetWidth;
}
};
const calculateTablePositionAndWidth = () => {
calculateTablePosition();
calculateTableWidth();
};
React.useEffect(() => {
calculateTablePositionAndWidth();
}, []);
React.useEffect(() => {
window.addEventListener('scroll', calculateTablePosition);
window.addEventListener('resize', calculateTablePositionAndWidth);
return () => {
window.removeEventListener('scroll', calculateTablePosition);
window.removeEventListener('resize', calculateTablePositionAndWidth);
};
}, [isActivelyViewed(ref.current)]);
return shouldHeaderBeFixed;
};
// initiallize our custom hook
const shouldHeaderBeFixed = useShouldHeaderBeFixed(ProductTableRef);
// listen only to our custom hook to set global state for the fixed header
React.useEffect(() => {
setIsHeaderActivelyFixed(shouldHeaderBeFixed);
}, [shouldHeaderBeFixed, globalState.isHeaderFixedOnScroll]);
...