I have a problem with the localStorage in my application. When I add items to a list of "favorites" they are stored without any problem in the localStorage, they can even be deleted by clicking them again.
But when I refresh the page, my application doesn't read that these items are in the favorites list and therefore doesn't mark them. Also, when I add a new item to the favorites list it causes it to delete everything from localStorage and start over.
Here's a gif of the localStorage view
Here's the code:
import React, { useState, useEffect } from 'react';
import SearchBar from '../../SearchBar/SearchBar.js';
import FiltersBox from '../FiltersBox/FiltersBox.js';
import { getItems } from '../../../Database/Database.js';
import './ItemsContainer.css';
function ItemsContainer() {
const [items, setItems] = useState([]);
const [search, setSearch] = useState('');
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
const [sortPrice, setSortPrice] = useState('');
const [filterCategory, setFilterCategory] = useState('');
const addItemToFavorites = item => {
let existentItem = favoriteItems.find(favItem => favItem.id === item.id);
if (existentItem) {
let filterTheExistentItem = favoriteItems.filter(
favItem => item.title !== favItem.title
);
setFavoriteItems(filterTheExistentItem);
let stringItems = JSON.stringify(filterTheExistentItem);
localStorage.setItem('Favorite Items', stringItems);
} else {
setFavoriteItems([...favoriteItems, item]);
let stringItems = JSON.stringify([...favoriteItems, item]);
localStorage.setItem('Favorite Items', stringItems);
}
};
const filteredItemsList = () => {
let newItemList = [];
newItemList = items.filter(item => {
if (filterCategory !== '' && filterCategory !== 'none') {
return item.category === filterCategory;
} else {
return item;
}
});
if (sortPrice === 'ascending') {
return newItemList.sort((a, b) => (a.price > b.price ? 1 : -1));
} else if (sortPrice === 'descending') {
return newItemList.sort((a, b) => (b.price > a.price ? 1 : -1));
} else {
return newItemList;
}
};
function onSortSelected(sortValue) {
setSortPrice(sortValue);
}
function onCategorySelected(categoryValue) {
setFilterCategory(categoryValue);
}
useEffect(() => {
getItems().then(res => setItems(res));
}, []);
useEffect(() => {
let xd = JSON.parse(localItems);
console.log(xd);
}, [localItems]);
return (
<div>
<SearchBar setSearch={setSearch} />
<FiltersBox
items={items}
setItems={setItems}
onSortSelected={onSortSelected}
onCategorySelected={onCategorySelected}
/>
<div>
{filteredItemsList()
.filter(item =>
search.toLowerCase() === ''
? item
: item.title.toLowerCase().includes(search)
)
.map(item => (
<div key={item.id}>
<div>{item.title}</div>
<button
className={favoriteItems.includes(item) ? 'si' : 'no'}
onClick={() => addItemToFavorites(item)}>
Add to favorites
</button>
</div>
))}
</div>
</div>
);
}
export default ItemsContainer;
And here I leave a GIF with a continuous console.log of the localStorage:
I tried everyrhing, and I don't know what is happening.
You're retrieving your items in localItems and... you do nothing with this variable. You should initialize your state favoritesItems with your local storage
const getItemsFromLocalStorage = () => {
const items = localStorage.getItem('Favorite Items');
return items ? JSON.parse(items) : [];
}
const [favoriteItems, setFavoriteItems] = useState(getItemsFromLocalStorage())
This is where the culprit is:
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
You load localStorage into localItems, but you expect it to be in favoriteItems, where you have never assigned it. You would need to specify the item of localStorage as the initial state, like:
let localItems = localStorage.getItem('Favorite Items');
const [favoriteItems, setFavoriteItems] = useState(localItems ? localItems : []);
So I'm not sure why my code isn't taking into subsequent keydowns, it takes the first 'l' then just stops:
I honestly don't know what the issue is. The lorem is a long string that I turn into an array, then if "e.key" === array item at 0, remove that item and update position.
I don't know why this isn't being done for every keydown.
import { useState } from "react"
const Input = ({text}) => {
const [lorem,changeString] = useState(text.split(''))
const [input,changeInput] = useState('')
const [position,changePosition] = useState(0)
const [color,changeColor] = useState(false)
const keyDown = (e) => {
changeInput(e.key)
compareCharacters(input)
}
const compareCharacters = (input) => {
if (input === lorem[position]) {
changeString(lorem.splice(0,1))
changePosition(position+1)
} else {
changeColor(true)
}
}
return (
<input className={`text ${color ? 'new' : ''}`}
type="text"
size="10000"
onKeyDown={keyDown}
>
</input>
)
}
export default Input
import { useState } from 'react'
const Input = ({ text }) => {
const [lorem, changeString] = useState(text.split(''))
const [position, changePosition] = useState(0)
const [color, changeColor] = useState(false)
const keyDown = (e) => {
if (e.key !== lorem[position]) {
changeColor(true)
return
}
changeString(lorem.slice(1))
changePosition(position + 1)
}
return (
<input
className={`text ${color ? 'new' : ''}`}
type="text"
size={10000}
onKeyDown={keyDown}
></input>
)
}
export default Input
I am creating to-do app in react and for the id of task i am using generator function. But This generator function is giving value 0 everytime and not incrementing the value.I think the reason for issue is useCallback() hook but i am not sure what can be the solution.How to solve the issue?Here i am providing the code :
import DateAndDay, { date } from "../DateAndDay/DateAndDay";
import TaskList, { TaskProps } from "../TaskList/TaskList";
import "./ToDo.css";
import Input from "../Input/Input";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
function ToDo() {
const [inputShow, setInputShow] = useState(false);
const [valid, setValid] = useState(false);
const [enteredTask, setEnteredTask] = useState("");
const [touched, setTouched] = useState(false);
const [tasks, setTasks] = useState<TaskProps[]>(() => {
let list = localStorage.getItem("tasks");
let newdate = String(date);
const setdate = localStorage.getItem("setdate");
if (newdate !== setdate) {
localStorage.removeItem("tasks");
}
if (list) {
return JSON.parse(list);
} else {
return [];
}
});
const activeHandler = (id: number) => {
const index = tasks.findIndex((task) => task.id === id);
const updatedTasks = [...tasks];
updatedTasks[index].complete = !updatedTasks[index].complete;
setTasks(updatedTasks);
};
const clickHandler = () => {
setInputShow((prev) => !prev);
};
const input = inputShow && (
<Input
checkValidity={checkValidity}
enteredTask={enteredTask}
valid={valid}
touched={touched}
/>
);
const btn = !inputShow && (
<button className="add-btn" onClick={clickHandler}>
+
</button>
);
function checkValidity(e: ChangeEvent<HTMLInputElement>) {
setEnteredTask(e.target.value);
}
function* idGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
let id = idGenerator();
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
setTouched(true);
if (enteredTask === "") {
setValid(false);
} else {
setValid(true);
const newtitle = enteredTask;
const newComplete = false;
const obj = {
id: Number(id.next().value),
title: newtitle,
complete: newComplete,
};
setTasks([...tasks, obj]);
localStorage.setItem("setdate", date.toString());
setEnteredTask("");
}
},
[enteredTask, tasks, id]
);
useEffect(() => {
const handleKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setInputShow(false);
}
if (event.key === "Enter") {
submitHandler(event);
}
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, [submitHandler]);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
return (
<div className="to-do">
<DateAndDay />
<TaskList tasks={tasks} activeHandler={activeHandler} />
{input}
{btn}
</div>
);
}
export default ToDo;
useCallBack()'s is used to memorize the result of function sent to it. This result will never change until any variable/function of dependency array changes it's value. So, please check if the dependencies passed are correct or if they are changing in your code or not ( or provide all the code of this file). One of my guess is to add the Valid state as dependency to the array
It's because you are calling the idGenerator outside of the useCallback, so it is only generated if the Component is re-rendered, in your case... only once.
Transfer it inside useCallback and call it everytime the event is triggered:
// wrap this on a useCallback so it gets memoized
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let id = idGenerator();
// ... rest of logic
},
[enteredTask, tasks, idGenerator]
);
If you're using the generated id outside the event handler, store the id inside a state like so:
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const [id, setId] = useState(idGenerator());
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let newId = idGenerator();
setId(newId)
// ... rest of logic
},
[enteredTask, tasks, id, idGenerator]
);
I have an 'Autocomplete' grandchild React component which takes data from the child component, helps the user autocomplete a form field, and then passes the value right back up to the grandparent component which posts the whole form.
I am able to get data from grandchild to child, but not able to get the data from there up to the grandparent.
Grandparent - AddPost.jsx
import React, { useState } from 'react';
import GetBeerStyle from './GetBeerStyle';
export default function AddPost(props) {
const [parentBeerStyleData, setParentBeerStyleData] = useState("")
const handleSubmit = (e) => {
e.preventDefault();
// There's some code here that pulls the data together and posts to the backend API
}
return (
<div>
<GetBeerStyle
name="beerstyle"
beerStyleData={childData => setParentBeerStyleData(childData)}
onChange={console.log('Parent has changed')}
/>
// More data input fields are here...
</div>
);
}
Child - GetBeerStyle.jsx
import React, {useState, useEffect } from 'react';
import axios from 'axios';
import Autocomplete from '../Autocomplete';
export default function GetBeerStyle(props) {
const [beerStyleData, setBeerStyleData] = useState("")
const [beerStyles, setBeerStyles] = useState(null)
const apiURL = "http://localhost:8080/api/styles/"
// Code to fetch the beer styles from the API and push down to the grandchild
// component to enable autocomplete
const fetchData = async () => {
const response = await axios.get(apiURL)
const fetchedStyles = Object.values(response.data)
const listOfStyles = []
for (let i = 0; i < fetchedStyles.length; i++) {
(listOfStyles[i] = fetchedStyles[i].style_name)
}
setBeerStyles(listOfStyles)
};
// This snippet pulls data from the Posts table via the API when this function is called
useEffect(() => {
fetchData();
}, []);
return (
<div className="one-cols">
<Autocomplete
suggestions={ beerStyles } // sending the data down to gchild
parentUpdate={childData => setBeerStyleData(childData)}// passing data back to gparent
onChange={() => props.beerStyleData(beerStyleData)}
/>
</div>
);
}
Grandchild - Autocomplete.jsx
import React, { Component, Fragment, useState } from 'react';
export default function Autocomplete(props) {
const [activeSuggestion, setActiveSuggestion] = useState(0);
const [filteredSuggestions, setFilteredSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [userInput, setUserInput] = useState("");
const [fieldId, setFieldId] = useState("");
const [parentUpdate, setParentUpdate] = useState("");
const onChange = e => {
const { suggestions } = props;
setActiveSuggestion(0);
setFilteredSuggestions(suggestions.filter(suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) >-1));
setShowSuggestions(true);
setUserInput(e.currentTarget.value);
setParentUpdate(e.currentTarget.value);
(console.log(parentUpdate));
return props.parentUpdate(parentUpdate);
};
const onClick = e => {
setActiveSuggestion(0);
setFilteredSuggestions([]);
setShowSuggestions(false);
setUserInput(e.currentTarget.innerText);
setFieldId(props.fieldId);
setParentUpdate(e.currentTarget.innerText);
return props.parentUpdate(parentUpdate);
};
const onKeyDown = e => {
// User pressed the ENTER key
if (e.keyCode === 13) {
setActiveSuggestion(0);
setShowSuggestions(false);
setUserInput(filteredSuggestions[activeSuggestion]);
// User pressed the UP arrow
} else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
setActiveSuggestion(activeSuggestion - 1);
}
// User pressed the DOWN arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
setActiveSuggestion(activeSuggestion + 1);
}
};
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No Suggestions Available.</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
value={userInput}
onChange={onChange}
onKeyDown={onKeyDown}
id={fieldId}
/>
<div>
{suggestionsListComponent}
</div>
</Fragment>
);
}
While I certainly accept there may be other issues with the code, overriding problem that I seem to have spent an inordinate amount of time googling and researching, is that I can't get the data being entered in the form input, to pull through to the grandparent component!
What have I missed?
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];
}