我正在构建一个表单 - 用户需要回答一系列问题(单选按钮),然后才能进入下一个屏幕。对于字段验证,我使用 yup (npm 包)和 redux 作为状态管理。
对于一种特定场景/组合,会显示一个新屏幕 (div),要求用户确认(复选框),然后用户才能继续。我想仅在显示时应用此复选框的验证。
如何使用 React 检查元素(div)是否显示在 DOM 中?
我的想法是将变量“isScreenVisible”设置为 false,如果满足条件,我会将状态更改为“true”。
我正在检查并在 _renderScreen() 中将“isScreenVisible”设置为 true 或 false,但由于某种原因它会进入无限循环。
我的代码:
class Component extends React.Component {
constructor(props) {
super(props);
this.state = {
formisValid: true,
errors: {},
isScreenVisible: false
}
this.FormValidator = new Validate();
this.FormValidator.setValidationSchema(this.getValidationSchema());
}
areThereErrors(errors) {
var key, er = false;
for(key in errors) {
if(errors[key]) {er = true}
}
return er;
}
getValidationSchema() {
return yup.object().shape({
TravelInsurance: yup.string().min(1).required("Please select an option"),
MobilePhoneInsurance: yup.string().min(1).required("Please select an option"),
Confirmation: yup.string().min(1).required("Please confirm"),
});
}
//values of form fields
getValidationObject() {
let openConfirmation = (this.props.store.Confirmation === true)? 'confirmed': ''
return {
TravelInsurance: this.props.store.TravelInsurance,
MobilePhoneInsurance: this.props.store.MobilePhoneInsurance,
Confirmation: openConfirmation,
}
}
setSubmitErrors(errors) {
this.setState({errors: errors});
}
submitForm() {
var isErrored, prom, scope = this, obj = this.getValidationObject();
prom = this.FormValidator.validateSubmit(obj);
prom.then((errors) => {
isErrored = this.FormValidator.isFormErrored();
scope.setState({errors: errors}, () => {
if (isErrored) {
} else {
this.context.router.push('/Confirm');
}
});
});
}
saveData(e) {
let data = {}
data[e.target.name] = e.target.value
this.props.addData(data)
this.props.addData({
Confirmation: e.target.checked
})
}
_renderScreen = () => {
const {
Confirmation
} = this.props.store
if(typeof(this.props.store.TravelInsurance) !== 'undefined' && typeof(this.props.store.MobilePhoneInsurance) !== 'undefined') &&
((this.props.store.TravelInsurance === 'Yes' && this.props.store.MobilePhoneInsurance === 'No') ||
(this.props.store.TravelInsurance === 'No' && this.props.store.MobilePhoneInsurance === 'Yes')){
this.setState({
isScreenVisible: true
})
return(
<div>
<p>Please confirm that you want to proceed</p>
<CheckboxField
id="Confirmation"
name="Confirmation"
value={Confirmation}
validationMessage={this.state.errors.Confirmation}
label="I confirm that I would like to continue"
defaultChecked={!!Confirmation}
onClick={(e)=> {this.saveData(e)} }
/>
</FormLabel>
</div>
)
}
else{
this.setState({
isScreenVisible: false
})
}
}
render(){
const {
TravelInsurance,
MobilePhoneInsurance
} = this.props.store
return (
<div>
<RadioButtonGroup
id="TravelInsurance"
name="TravelInsurance"
checked={TravelInsurance}
onClick={this.saveData.bind(this)}
options={{
'Yes': 'Yes',
'No': 'No'
}}
validationMessage={(this.state.errors.TravelInsurance) ? this.state.errors.TravelInsurance : null }
/>
<RadioButtonGroup
id="MobilePhoneInsurance"
name="MobilePhoneInsurance"
checked={MobilePhoneInsurance}
onClick={this.saveData.bind(this)}
options={{
'Yes': 'Yes',
'No': 'No'
}}
validationMessage={(this.state.errors.MobilePhoneInsurance) ? this.state.errors.MobilePhoneInsurance : null }
/>
this._renderScreen()
<ButtonRow
primaryProps={{
children: 'Continue',
onClick: e=>{
this.submitForm();
}
}}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state.Insurance,
}
}
const Insurance = connect(mapStateToProps,{addData})(Component)
export default Insurance
这是一个可重用的钩子,它利用了 IntersectionObserver API。
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const [isIntersecting, setIntersecting] = useState(false)
const observer = useMemo(() => new IntersectionObserver(
([entry]) => setIntersecting(entry.isIntersecting)
), [ref])
useEffect(() => {
observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return isIntersecting
}
const DummyComponent = () => {
const ref = useRef<HTMLDivElement>(null)
const isVisible = useOnScreen(ref)
return <div ref={ref}>{isVisible && `Yep, I'm on screen`}</div>
}
您可以将引用附加到要检查它是否在视口上的元素,然后使用类似以下内容:
/**
* Check if an element is in viewport
*
* @param {number} [offset]
* @returns {boolean}
*/
isInViewport(offset = 0) {
if (!this.yourElement) return false;
const top = this.yourElement.getBoundingClientRect().top;
return (top + offset) >= 0 && (top - offset) <= window.innerHeight;
}
render(){
return(<div ref={(el) => this.yourElement = el}> ... </div>)
}
您可以附加诸如
onScroll
之类的侦听器并检查元素何时出现在视口上。
根据 Avraam 的回答,我编写了一个兼容 Typescript 的小钩子来满足实际的 React 代码约定。
import { useRef, useEffect, useState } from "react";
import throttle from "lodash.throttle";
/**
* Check if an element is in viewport
* @param {number} offset - Number of pixels up to the observable element from the top
* @param {number} throttleMilliseconds - Throttle observable listener, in ms
*/
export default function useVisibility<Element extends HTMLElement>(
offset = 0,
throttleMilliseconds = 100
): [Boolean, React.RefObject<Element>] {
const [isVisible, setIsVisible] = useState(false);
const currentElement = useRef<Element>();
const onScroll = throttle(() => {
if (!currentElement.current) {
setIsVisible(false);
return;
}
const top = currentElement.current.getBoundingClientRect().top;
setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight);
}, throttleMilliseconds);
useEffect(() => {
document.addEventListener('scroll', onScroll, true);
return () => document.removeEventListener('scroll', onScroll, true);
});
return [isVisible, currentElement];
}
使用示例:
const Example: FC = () => {
const [ isVisible, currentElement ] = useVisibility<HTMLDivElement>(100);
return <Spinner ref={currentElement} isVisible={isVisible} />;
};
您可以在Codesandbox上找到示例。 我希望你会发现它有帮助!
@Alex Gusev 没有 lodash 并使用 useRef 回答
import { MutableRefObject, useEffect, useRef, useState } from 'react'
/**
* Check if an element is in viewport
* @param {number} offset - Number of pixels up to the observable element from the top
*/
export default function useVisibility<T>(
offset = 0,
): [boolean, MutableRefObject<T>] {
const [isVisible, setIsVisible] = useState(false)
const currentElement = useRef(null)
const onScroll = () => {
if (!currentElement.current) {
setIsVisible(false)
return
}
const top = currentElement.current.getBoundingClientRect().top
setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight)
}
useEffect(() => {
document.addEventListener('scroll', onScroll, true)
return () => document.removeEventListener('scroll', onScroll, true)
})
return [isVisible, currentElement]
}
使用示例:
const [beforeCheckoutSubmitShown, beforeCheckoutSubmitRef] = useVisibility<HTMLDivElement>()
return (
<div ref={beforeCheckoutSubmitRef} />
在使用 TypeScript 尝试不同的建议解决方案后,由于第一次渲染将默认的
useRef
设置为 null
,我们一直面临错误。
这里有我们的解决方案,以防它对其他人有帮助😊
useInViewport.ts
:
import React, { useCallback, useEffect, useState } from "react";
export function useInViewport(): { isInViewport: boolean; ref: React.RefCallback<HTMLElement> } {
const [isInViewport, setIsInViewport] = useState(false);
const [refElement, setRefElement] = useState<HTMLElement | null>(null);
const setRef = useCallback((node: HTMLElement | null) => {
if (node !== null) {
setRefElement(node);
}
}, []);
useEffect(() => {
if (refElement && !isInViewport) {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && setIsInViewport(true)
);
observer.observe(refElement);
return () => {
observer.disconnect();
};
}
}, [isInViewport, refElement]);
return { isInViewport, ref: setRef };
}
SomeReactComponent.tsx
:
import { useInViewport } from "../layout/useInViewport";
export function SomeReactComponent() {
const { isInViewport, ref } = useInViewport();
return (
<>
<h3>A component which only renders content if it is in the current user viewport</h3>
<section ref={ref}>{isInViewport && (<ComponentContentOnlyLoadedIfItIsInViewport />)}</section>
</>
);
}
解决方案感谢@isma-navarro😊
我也遇到了同样的问题,而且,看起来,我在纯 React jsx 中找到了非常好的解决方案,无需安装任何库。
import React, {Component} from "react";
class OurReactComponent extends Component {
//attach our function to document event listener on scrolling whole doc
componentDidMount() {
document.addEventListener("scroll", this.isInViewport);
}
//do not forget to remove it after destroyed
componentWillUnmount() {
document.removeEventListener("scroll", this.isInViewport);
}
//our function which is called anytime document is scrolling (on scrolling)
isInViewport = () => {
//get how much pixels left to scrolling our ReactElement
const top = this.viewElement.getBoundingClientRect().top;
//here we check if element top reference is on the top of viewport
/*
* If the value is positive then top of element is below the top of viewport
* If the value is zero then top of element is on the top of viewport
* If the value is negative then top of element is above the top of viewport
* */
if(top <= 0){
console.log("Element is in view or above the viewport");
}else{
console.log("Element is outside view");
}
};
render() {
// set reference to our scrolling element
let setRef = (el) => {
this.viewElement = el;
};
return (
// add setting function to ref attribute the element which we want to check
<section ref={setRef}>
{/*some code*/}
</section>
);
}
}
export default OurReactComponent;
我试图弄清楚如何为视口中的元素设置动画。
这是基于
Creaforge
的答案,但针对您想要检查组件是否可见(并且在 TypeScript 中)的情况进行了更优化。
function useWasSeen() {
// to prevents runtime crash in IE, let's mark it true right away
const [wasSeen, setWasSeen] = React.useState(
typeof IntersectionObserver !== "function"
);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (ref.current && !wasSeen) {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && setWasSeen(true)
);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}
}, [wasSeen]);
return [wasSeen, ref] as const;
}
const ExampleComponent = () => {
const [wasSeen, ref] = useWasSeen();
return <div ref={ref}>{wasSeen && `Lazy loaded`}</div>
}
请记住,如果您的组件没有在调用钩子的同时安装,您将不得不使此代码变得更加复杂。就像将依赖数组变成
[wasSeen, ref.current]
@Creaforge 的 Intersection Observer 方法基于 TypeScript 的方法,修复了如果在挂载元素之前调用钩子则
ref.current
可能未定义的问题:
export default function useOnScreen<Element extends HTMLElement>(): [
boolean,
React.RefCallback<Element>,
] {
const [intersecting, setIntersecting] = useState(false);
const observer = useMemo(
() => new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting)),
[setIntersecting],
);
const currentElement = useCallback(
(ele: Element | null) => {
if (ele) {
observer.observe(ele);
} else {
observer.disconnect();
setIntersecting(false);
}
},
[observer, setIntersecting],
);
return [intersecting, currentElement];
}
用途:
const [endOfList, endOfListRef] = useOnScreen();
...
return <div ref={endOfListRef} />
基于@GuCier的评论。 我们可以添加另一个参数来指定容器元素。 这对于模态、表格、列表、任何带有滚动的非主体元素可能很有用。
import { useEffect, useMemo, useState } from 'react';
// A custom hook that receives an element and an optional
// parent element (defaults to body) and returns a boolean
// value based on whether the element in question
// is visible inside the said element.
// This information is useful if we want to prevent unnecessary
// image or complex component renders.
export const useIsVisible = (elRef, containerRef) => {
const [isIntersecting, setIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(
([entry]) => setIntersecting(entry.isIntersecting),
{ root: containerRef?.current }
),
[elRef, containerRef] //eslint-disable-line
);
useEffect(() => {
observer.observe(elRef.current);
return () => observer.disconnect();
}, []); //eslint-disable-line
return isIntersecting;
};
支持使用滚动条快速滚动,无视觉错误,打字稿友好:
import {useEffect, useState, useRef, RefObject} from 'react';
/**
* Custom Hook to determine if an element is in the viewport.
* @param rootMargin Margin around the root, similar to the CSS margin property.
* @returns A boolean state indicating visibility.
*/
export const useInView = (ref: RefObject<HTMLElement>, rootMargin: string = '0px') => {
const [isInView, setInView] = useState(false);
// Use a single instance of IntersectionObserver
const observerRef = useRef<IntersectionObserver>();
useEffect(() => {
if (observerRef.current) {
observerRef.current.disconnect(); // Disconnect previous observer if it exists
}
observerRef.current = new IntersectionObserver((entries) => {
// Set state based on whether the element is intersecting
setInView(entries[0].isIntersecting);
}, { rootMargin });
const { current } = ref;
if (current) {
observerRef.current.observe(current);
}
return () => {
observerRef.current?.disconnect();
};
}, [ref, rootMargin]); // Re-run effect if ref or rootMargin changes
return isInView;
};
观察元素In | 输出视口
import { useEffect, useRef } from 'react'
const MyCard = () => {
const cardRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([ent]: IntersectionObserverEntry[]) => {
alert(ent.isIntersecting ? 'In' : 'Out')
},
{ threshold: [1] }
)
observer.observe(cardRef.current)
return () => observer.disconnect()
}, [])
return <div ref={cardRef}>This is an observable Element</div>
}
export default MyCard
根据@Alex Gusev 的帖子回答
React hook,通过一些修复并基于 rxjs 库来检查元素是否可见。
import React, { useEffect, createRef, useState } from 'react';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, throttleTime } from 'rxjs/operators';
/**
* Check if an element is in viewport
* @param {number} offset - Number of pixels up to the observable element from the top
* @param {number} throttleMilliseconds - Throttle observable listener, in ms
* @param {boolean} triggerOnce - Trigger renderer only once when element become visible
*/
export default function useVisibleOnScreen<Element extends HTMLElement>(
offset = 0,
throttleMilliseconds = 1000,
triggerOnce = false,
scrollElementId = ''
): [boolean, React.RefObject<Element>] {
const [isVisible, setIsVisible] = useState(false);
const currentElement = createRef<Element>();
useEffect(() => {
let subscription: Subscription | null = null;
let onScrollHandler: (() => void) | null = null;
const scrollElement = scrollElementId
? document.getElementById(scrollElementId)
: window;
const ref = currentElement.current;
if (ref && scrollElement) {
const subject = new Subject();
subscription = subject
.pipe(throttleTime(throttleMilliseconds))
.subscribe(() => {
if (!ref) {
if (!triggerOnce) {
setIsVisible(false);
}
return;
}
const top = ref.getBoundingClientRect().top;
const visible =
top + offset >= 0 && top - offset <= window.innerHeight;
if (triggerOnce) {
if (visible) {
setIsVisible(visible);
}
} else {
setIsVisible(visible);
}
});
onScrollHandler = () => {
subject.next();
};
if (scrollElement) {
scrollElement.addEventListener('scroll', onScrollHandler, false);
}
// Check when just loaded:
onScrollHandler();
} else {
console.log('Ref or scroll element cannot be found.');
}
return () => {
if (onScrollHandler && scrollElement) {
scrollElement.removeEventListener('scroll', onScrollHandler, false);
}
if (subscription) {
subscription.unsubscribe();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [offset, throttleMilliseconds, triggerOnce, scrollElementId]);
return [isVisible, currentElement];
}