I'm building a form - series of questions (radio buttons) the user needs to answer before he can move on to the next screen. For fields validation I'm using yup (npm package) and redux as state management.
For one particular scenario/combination a new screen (div) is revealed asking for a confirmation (checkbox) before the user can proceed. I want to apply the validation for this checkbox only if displayed.
How can I check if an element (div) is displayed in the DOM using React?
The way I thought of doing it was to set a varibale 'isScreenVisible' to false and if the conditions are met I would change the state to 'true'.
I'm doing that check and setting 'isScreenVisible' to true or false in _renderScreen() but for some reason it's going into an infinite loop.
My code:
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
Here is a reusable hook that takes advantage of the IntersectionObserver API.
The hook
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
}
Usage
const DummyComponent = () => {
const ref = useRef<HTMLDivElement>(null)
const isVisible = useOnScreen(ref)
return <div ref={ref}>{isVisible && `Yep, I'm on screen`}</div>
}
You can attach a ref to the element that you want to check if it is on the viewport and then have something like:
/**
* 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>)
}
You can attach listeners like onScroll and check when the element will be on the viewport.
You can also use the Intersection Observer API with a polyfil or use a HoC component that does the job
Based on Avraam's answer I wrote a Typescript-compatible small hook to satisfy the actual React code convention.
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];
}
Usage example:
const Example: FC = () => {
const [ isVisible, currentElement ] = useVisibility<HTMLDivElement>(100);
return <Spinner ref={currentElement} isVisible={isVisible} />;
};
You can find the example on Codesandbox.
I hope you will find it helpful!
#Alex Gusev answer without lodash and using 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]
}
usage example:
const [beforeCheckoutSubmitShown, beforeCheckoutSubmitRef] = useVisibility<HTMLDivElement>()
return (
<div ref={beforeCheckoutSubmitRef} />
I have had the same problem, and, looks, I found the pretty good solution in pure react jsx, without installing any libraries.
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;
I was trying to figure out how to animate elements if the are in viewport.
Here is work project on CodeSandbox.
This is based on the answer from Creaforge but more optimized for the case when you want to check if the component has become visible (and in TypeScript).
Hook
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;
}
Usage
const ExampleComponent = () => {
const [wasSeen, ref] = useWasSeen();
return <div ref={ref}>{wasSeen && `Lazy loaded`}</div>
}
Keep in mind that if your component is not mounted at the same time as the hook is called you would have to make this code more complicated. Like turning dependency array into [wasSeen, ref.current]
After trying out the different proposed solutions with TypeScript, we have been facing errors due to the first render setting the default useRef to null.
Here you have our solution just in case it helps other people 😊
The hook
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 };
}
Usage
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>
</>
);
}
Solution thanks to #isma-navarro 😊
TypeScript based approach to #Creaforge's Intersection Observer approach, that fixes the issue with ref.current being potentially undefined if the hook was called before the element is mounted:
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];
}
Usage:
const [endOfList, endOfListRef] = useOnScreen();
...
return <div ref={endOfListRef} />
Answer based on the post from #Alex Gusev
React hook to check whether the element is visible with a few fixes and based on the rxjs library.
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];
}
Related
My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.
I am trying to calculate the width of child component .is it possible to calculate width of children's ? here is my code
here is my code
https://codesandbox.io/s/reverent-hermann-yt7es?file=/src/App.js
<Tabs>
{data.map((i) => (
<li>{i}</li>
))}
</Tabs>
TABS.js
import React, { useEffect, useRef } from "react";
const Tabs = ({ children }) => {
const tabsRef = useRef(null);
const setTabsDimensions = () => {
if (!tabsRef.current) {
return;
}
// initial wrapper width calculation
const blockWidth = tabsRef.current.offsetWidth;
// const showMoreWidth = moreItemRef.current.offsetWidth;
// calculate width and offset for each tab
let tabsTotalWidth = 0;
const tabDimensions = {};
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
// const width = !isMobile ? 200 : 110;
// tabDimensions[index] = {
// width,
// offset: tabsTotalWidth
// };
// tabsTotalWidth += width;
}
});
};
useEffect(() => {
setTabsDimensions();
});
return (
<ul ref={tabsRef} className="rc64nav">
{children}
</ul>
);
};
export default Tabs;
I know using ref we can access the dom property but how to apply ref in children element element.
here i am trying to calculate all item width
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
Just answer your question how to get child's ref.
const Parent = () => {
const childRef = useRef()
// An example of use the childRef
const onClick = () => {
if (!childRef.current) return
console.log(childRef.current.offsetWidth)
}
return <Child childRef={childRef} />
}
const Child = ({ childRef }) => {
return <div ref={childRef}>Hello Child</div>
}
NOTE: the way I pass ref here is exactly the same way you pass parent method to child. And also ref supports two formats, i'm using the simple one, but you can also use () => {} format. https://reactjs.org/docs/refs-and-the-dom.html
I'm using memoize-one on a React component that is basically a table with a rows that can be filtered.
Memoize works great for the filtering but when I want to insert a new row, it won't show up on the table until I either reload the page or use the filter.
If I check the state, the new row's data is in it, so presumably what is happening is that memoize is not allowing the component to re-render even if the state has changed.
Something interesting is that the Delete function works, I am able to delete a row by removing its data from the state and it will re-render to reflect the changes...
Here's the part of the code I consider relevant but if you would like to see more, let me know:
import React, { Component } from "react";
import memoize from "memoize-one";
import moment from "moment";
import {
Alert,
Card,
Accordion,
Button,
Table,
Spinner,
} from "react-bootstrap";
import PropTypes from "prop-types";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";
import RoleMember from "./RoleMember";
import CreateMemberModal from "./CreateMemberModal";
class RoleContainer extends Component {
filter = memoize((roleMembers, searchValue, searchCriterion) => {
const searchBy = searchCriterion || "alias";
return roleMembers.filter((item) => {
if (item[searchBy]) {
if (searchValue === "") {
return true;
}
const value = searchValue.toLowerCase();
if (searchBy !== "timestamp") {
const target = item[searchBy].toLowerCase();
return target.includes(value);
}
// Case for timestamp
const target = moment(Number(item[searchBy]))
.format("MMM DD, YYYY")
.toLowerCase();
return target.includes(value);
}
return false;
});
});
constructor(props) {
super(props);
this.state = {
collapsed: true,
roleAttributes: [],
roleMembers: [],
isLoading: true,
};
}
componentDidMount = async () => {
const roleMembers = Object.values(await this.fetchRoleMembers());
roleMembers.forEach((e) => {
e.alias = e.alias.toLowerCase();
return null;
});
roleMembers.sort((a, b) => {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
});
// TODO - This logic should be replaced with an API call that describes the roleAttributes.
let roleAttributes = Object.values(roleMembers);
roleAttributes = Object.keys(roleAttributes[0]);
this.setState({
roleMembers,
roleAttributes,
isLoading: false,
});
};
fetchRoleMembers = async () => {
const { roleAttributeName } = this.props;
return getRoleMembersDetailed(roleAttributeName);
};
createRoleMember = (newRoleMembers) => {
const { roleMembers } = this.state;
newRoleMembers.forEach((e) => {
roleMembers.push(e);
});
this.setState(
() => {
roleMembers.sort((a, b) => {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
});
return { roleMembers };
},
() => {
console.log("sss", this.state);
}
);
};
deleteRoleMember = (alias) => {
this.setState((prevState) => {
const { roleMembers } = prevState;
return {
roleMembers: roleMembers.filter((member) => member.alias !== alias),
};
});
};
render() {
const {
role,
roleAttributeName,
searchValue,
searchCriterion,
userCanEdit,
} = this.props;
const { collapsed, isLoading, roleAttributes, roleMembers } =
this.state;
const filteredRoleMembers = this.filter(
roleMembers,
searchValue,
searchCriterion
);
return (
// continues...
I don't know if it's obvious but there are two functions called filter: this.filter that belongs to memoize and Array.prototype.filter().
I did look around and found these post that says Memoize can be overridden:
If you’ve ran into a UI bug, it is simple to just return false from myComparison to temporarily override the memoization, forcing a refresh on every re-render and returning to the default component behaviour.
But I'm not sure what they mean with "return false from component"
Here's a refactoring of your code to idiomatic React Hooks style (naturally dry-coded).
Note how filtering and sorting the role members is done using useMemo() in a way that doesn't modify state; that's because they can be always recomputed from the stateful data. So long as the useMemo()s' deps array is kept in sync (there're ESLint rules to help with this), this should work with no extra re-renders. :)
Similarly, if you use useCallback (which is a special case of useMemo), you need to keep their deps arrays in sync. If you don't use useCallback, those callbacks may cause re-renders since their identity changes per-render.
import React, { Component } from "react";
import moment from "moment";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";
function filterRoleMembers(
roleMembers,
searchValue,
searchCriterion,
) {
const searchBy = searchCriterion || "alias";
return roleMembers.filter((item) => {
if (item[searchBy]) {
if (searchValue === "") {
return true;
}
const value = searchValue.toLowerCase();
if (searchBy !== "timestamp") {
const target = item[searchBy].toLowerCase();
return target.includes(value);
}
// Case for timestamp
const target = moment(Number(item[searchBy]))
.format("MMM DD, YYYY")
.toLowerCase();
return target.includes(value);
}
return false;
});
}
// TODO: maybe use lodash's `sortBy`?
function compareByAlias(a, b) {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
}
async function fetchRoleMembers(roleAttributeName) {
return getRoleMembersDetailed(roleAttributeName);
}
async function loadData(roleAttributeName) {
const roleMembers = Object.values(
await fetchRoleMembers(roleAttributeName),
);
roleMembers.forEach((e) => {
e.alias = e.alias.toLowerCase();
});
// TODO - This logic should be replaced with an API call that describes the roleAttributes.
let roleAttributes = Object.values(roleMembers);
roleAttributes = Object.keys(roleAttributes[0]);
return {
roleMembers,
roleAttributes,
};
}
const RoleContainer = ({
role,
roleAttributeName,
searchValue,
searchCriterion,
userCanEdit,
}) => {
const [collapsed, setCollapsed] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(true);
const [roleAttributes, setRoleAttributes] = React.useState([]);
const [roleMembers, setRoleMembers] = React.useState([]);
React.useEffect(() => {
loadData(roleAttributeName).then(
({ roleMembers, roleAttributes }) => {
setRoleAttributes(roleAttributes);
setRoleMembers(roleMembers);
setIsLoading(false);
},
);
}, [roleAttributeName]);
const createRoleMember = React.useCallback(
(newRoleMembers) => {
const updatedRoleMembers = roleMembers.concat(newRoleMembers);
setRoleMembers(updatedRoleMembers);
},
[roleMembers],
);
const deleteRoleMember = React.useCallback(
(alias) => {
const updatedRoleMembers = roleMembers.filter(
(member) => member.alias !== alias,
);
setRoleMembers(updatedRoleMembers);
},
[roleMembers],
);
const filteredRoleMembers = React.useMemo(
() =>
filterRoleMembers(roleMembers, searchValue, searchCriterion),
[roleMembers, searchValue, searchCriterion],
);
const sortedRoleMembers = React.useMemo(
() => [].concat(filteredRoleMembers).sort(compareByAlias),
[filteredRoleMembers],
);
return <>{JSON.stringify(sortedRoleMembers)}</>;
};
I am trying to convert class component to functional component. but getting this error
Function components cannot have refs. Did you mean to use React.forwardRef()?
working example with class component
https://codesandbox.io/s/highlight-search-results-wqtlm?file=/src/components/Search/List.js
Now I change the list class component to function component
https://codesandbox.io/s/funny-bird-dkfpi?file=/src/components/List.js
my code breaks and give me below error
Function components cannot have refs. Did you mean to use React.forwardRef()?
please suggest how to fix this error
import React, { useEffect, useRef, useState} from 'react';
import User from './User';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const List = ({users})=>{
const [focusedUser, setFocusedUser] = useState(-1);
const [scrollIntoView, setScrollIntoView] = useState(false);
const userRef = useRef();
const prev = usePrevious({focusedUser});
useEffect(()=>{
if (
focusedUser !== prev.focusedUser &&
scrollIntoView
) {
ensureFocusedItemVisible();
}
},[focusedUser])
const ensureFocusedItemVisible =()=> {
userRef.current &&
userRef.current.scrollIntoView({
behaviour: 'smooth',
block: 'center'
});
}
const handleKeyPress = e => {
// scroll into view only on keyboard navigation
setScrollIntoView( true );
// up arrow
if (e.keyCode === 38) {
const fu = focusedUser <= 0 ? 0 : focusedUser - 1;
setFocusedUser(fu);
}
// down arrow
if (e.keyCode === 40) {
const currentFocus = focusedUser;
// if down key is pressed multiple times on last list item, keep last item highlighted
const fu =
currentFocus >= users.length - 1
? users.length - 1
: currentFocus + 1;
setFocusedUser(fu);
}
};
const handleMouseEvent = id => {
userRef.current && userRef.current.focus();
setScrollIntoView(false);
setFocusedUser(parseInt(id))
};
const listElements = users.map((user, index) => {
const focused = index === focusedUser;
return (
<User
divId={index}
data={user}
focused={focused}
ref={focused && userRef}
handleKeyPress={handleKeyPress}
handleMouseEvent={handleMouseEvent}
/>
);
});
return <div className="result-list">{listElements}</div>;
}
export default List;
You have 2 errors actually.
The first one is here:
ref={focused && this.userRef}
The second one is here:
const prev = usePrevious({ focusedUser });
useEffect(() => {
if (focusedUser !== prev.focusedUser && scrollIntoView) {
ensureFocusedItemVisible();
}
}, [focusedUser]);
And here is the fix with explanation in comments:
import React, { useEffect, useRef, useState } from "react";
import User from "./User";
const List = ({ users }) => {
const [focusedUser, setFocusedUser] = useState(-1);
const [scrollIntoView, setScrollIntoView] = useState(false);
const userRef = useRef();
// You don't need to keep track of the previous value.
// useEffect will be called on first render and whenever one of the values in the dependencies array changes
// So it will run whenever the focusedUser changes
useEffect(() => {
if (scrollIntoView) {
ensureFocusedItemVisible();
}
}, [focusedUser, scrollIntoView]);
const ensureFocusedItemVisible = () => {
userRef.current &&
userRef.current.scrollIntoView({
behaviour: "smooth",
block: "center"
});
};
const handleKeyPress = e => {
// scroll into view only on keyboard navigation
setScrollIntoView(true);
// up arrow
if (e.keyCode === 38) {
const fu = focusedUser <= 0 ? 0 : focusedUser - 1;
setFocusedUser(fu);
}
// down arrow
if (e.keyCode === 40) {
const currentFocus = focusedUser;
// if down key is pressed multiple times on last list item, keep last item highlighted
const fu =
currentFocus >= users.length - 1 ? users.length - 1 : currentFocus + 1;
setFocusedUser(fu);
}
};
const handleMouseEvent = id => {
userRef.current && userRef.current.focus();
setScrollIntoView(false);
setFocusedUser(parseInt(id));
};
const listElements = users.map((user, index) => {
const focused = index === focusedUser;
return (
<User
key={index}
divId={index}
data={user}
focused={focused}
// if it isn't focused pass null
// the ref should be a function or a ref object or null
ref={focused ? userRef : null}
handleKeyPress={handleKeyPress}
handleMouseEvent={handleMouseEvent}
/>
);
});
return <div className="result-list">{listElements}</div>;
};
export default List;
I'm using a React Context to store data and to provide functionality to modify these data.
Now, I'm trying to convert a Class Component into a Functional Component using React Hooks.
While everything is working as expected in the Class, I don't get it to work in the Functional Component.
Since my applications code is a bit more complex, I've created this small example (JSFiddle link), which allows to reproduce the problem:
First the Context, which is the same for both, the Class and the Functional Component:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
constructor (props) {
super(props);
this.increase = this.increase.bind(this);
this.reset = this.reset.bind(this);
this.state = {
current: 0,
increase: this.increase,
reset: this.reset
}
}
render () {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase (step) {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset () {
this.setState({
current: 0
});
}
}
Now, here is the Class component, which works just fine:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.increaseByOne = this.increaseByOne.bind(this);
}
componentDidMount () {
setInterval(this.increaseByOne, 1000);
}
render () {
const count = this.context;
return (
<div>{count.current}</div>
);
}
increaseByOne () {
const count = this.context;
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}
}
MyComponent.contextType = MyContext;
The expected result is, that it counts to 5, in an interval of one second - and then starts again from 0.
And here is the converted Functional Component:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, []);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Instead of resetting the counter at 5, it resumes counting.
The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.
The only way I get this to work, is to adjust the code on the following way:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, [count]);
React.useEffect(() => {
console.log('useEffect');
const interval = setInterval(increaseByOne, 1000);
return () => {
clearInterval(interval);
};
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Now, the increaseByOne callback is recreated on every change of the context, which also means that the effect is called every second.
The result is, that it clears the interval and sets a new one, on every change to the context (You can see that in the browser console).
This may work in this small example, but it changed the original logic, and has a lot more overhead.
My application does not rely on an interval, but it's listening for an event. Removing the event listener and adding it again later, would mean, that I may loose some events, if they are fired between the remove and the binding of the listener, which is done asynchronously by React.
Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?
I've created a fiddle here, to play around with the code above:
https://jsfiddle.net/Jens_Duttke/78y15o9p/
First solution is to put data is changing through time into useRef so it would be accessible by reference not by closure(as well as you access actual this.state in class-based version)
const MyComponent = (props) => {
const countByRef = React.useRef(0);
countByRef.current = React.useContext(MyContext);
React.useEffect(() => {
setInterval(() => {
const count = countByRef.current;
console.log(count.current);
if (count.current === 5) {
count.reset();
} else {
count.increase(1);
}
}, 1000);
}, []);
return (
<div>{countByRef.current.current}</div>
);
}
Another solution is to modify reset and increase to allow functional argument as well as it's possible with setState and useState's updater.
Then it would be
useEffect(() => {
setInterval(() => {
count.increase(current => current === 5? 0: current + 1);
}, 1000);
}, [])
PS also hope you have not missed clean up function in your real code:
useEffect(() => {
const timerId = setInterval(..., 1000);
return () => {clearInterval(timerId);};
}, [])
otherwise you will have memory leakage
If the increaseByOne function doesn't need to know the actual count.current, you can avoid recreating it. In the context create a new function called is that checks if the current is equal a value:
is = n => this.state.current === n;
And use this function in the increaseByOne function:
if (count.is(5)) {
count.reset();
}
Example:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase = (step) => {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset = () => {
this.setState({
current: 0
});
}
is = n => this.state.current === n;
state = {
current: 0,
increase: this.increase,
reset: this.reset,
is: this.is
};
}
const MyComponent = (props) => {
const { increase, reset, is, current } = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
if (is(5)) {
reset();
} else {
increase(1);
}
}, [increase, reset, is]);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{current}</div>
);
}
const App = () => (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
ReactDOM.render( <
App / > ,
document.querySelector("#app")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>