I rewrote my class component to a functional component because i had to use react hooks. Its all working fine now except for the screen. Its not updating the text. i've added a ref to the element called screen element and the function setScreenText seems to work. I put some logs and they return what i would expect from them. If I press key 5 console.log(screenElement.textContent) returns 5 and console.log(screenElement) returns {current: div.screen, textContent: "5"} The only problem still is that its not updating the text on the screen, it stays blank.
import React, {useRef, useState} from 'react';
import '../style/App.scss';
import {UPDATE_GAME_STATE} from '../actions';
import { useDispatch } from 'react-redux';
function Keypad() {
const dispatch = useDispatch();
const KEYPAD_STATE_SUCCESS = "keypad_success";
const KEYPAD_STATE_FAILED = "keypad_failed";
const KEYPAD_STATE_INPUT = "keypad_input";
const [state, setState] = useState(KEYPAD_STATE_INPUT);
const tries = [];
const screenElement = useRef(null);
const handleKeyPress = async(value) => {
tries.push(value);
setScreenText(tries.join(''));
if (tries.length >= 4) {
const success = await tryKeyCode(tries.join(''))
const newState = {
setState: success ? KEYPAD_STATE_SUCCESS : KEYPAD_STATE_FAILED
}
const screenText = success ? "SUCCESS" : "FAILED";
const textScreenTime = success ? 3000 : 1000;
const handleGameState = success ? () => onSuccess() : () => onFailed();
setTimeout(() => {
setScreenText(screenText);
setTimeout(handleGameState, textScreenTime);
}, 200);
setState(newState);
}
}
const onSuccess = () => {
dispatch(UPDATE_GAME_STATE('completed'))
}
const onFailed = () => {
console.log("wrong code")
}
/**
* #param {string} text
*/
const setScreenText = (text) => {
screenElement.textContent = text;
console.log(screenElement.textContent); // if key 5 is pressed returns 5
console.log(screenElement); // returns {current: div.screen, textContent: "5"}
}
/**
* #param {string} code
* #returns boolean
*/
const tryKeyCode = async(code) => {
const response = (await fetch("http://localhost:3000/keypad", {
method: "POST",
body: JSON.stringify({
"code": code
}),
headers: {
"Content-Type": "application/json"
}
}));
return response.status === 200;
}
const keys = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const Key = ({number, className = ""}) => <button className={"key " + className} onClick={() => handleKeyPress(number)}>{number}</button>
const keyElements = keys.map(key => (<Key number={key}/>))
return (
<div className="keypad_wrapper">
<div className="screen" ref={screenElement}/>
{keyElements}
<Key className="last" number={0}/>
</div>
);
}
export default Keypad;
As mentioned in the react docs:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Thus instead of updating the screenElement.textContent you need to update screenElement.current.textContent like:
const setScreenText = (text) => {
if (screenElement.current) {
// Wait until current is available
// `current` points to the mounted element
screenElement.current.textContent = text;
console.log(screenElement.current);
}
}
Related
I have been stuck on the simple issue of the common React setState delay. I am currently looking to update an object within an array, by saving it to a state variable "newStud" within a child component, and pass it into a parent component to be utilized for a filtering function. My current issue is that state only updates completely after the second submission of an entry on my site. Thus, when the filter function in the parent component aims to read the array being passed in, it throws errors as the initial declaration of state is what is passed in. My question is if there is some way I can adjust for that delay in updating that information without having to break apart my larger components into smaller more manageable components?
For reference, here is the code I am utilizing for the child component (the issue is present in my "addTag" function):
import React, {useState, useEffect} from 'react';
import './studentcard.css';
import { Tags } from '../Tags/tags.js';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faPlus } from '#fortawesome/free-solid-svg-icons';
import { faMinus } from '#fortawesome/free-solid-svg-icons';
export function StudentCard({student, upStuds}) {
const [newStud, setNewStud] = useState({});
const [clicked, setClicked] = useState(false);
const [tag, setTag] = useState('');
// switches boolean to opposite value for plus/minus icon display
const onClick = () => {
setClicked(!clicked);
};
// triggers the addTag function to push a tag to the array within the student object
const onSubmit = async (e) => {
e.preventDefault();
await addTag(tag);
};
// captures the values being entered into the input
const onChange = (e) => {
setTag(e.target.value);
};
// this pushes the tag state value into the array that is located in the student object being passed down from the parent component
// it is meant to save the new copy of the "student" value in "newStuds" state variable, and pass that into the callback func
// ********** here is where I am experiencing my delay ************
const addTag = () => {
student.tags.push(tag);
setNewStud({...student});
upStuds(newStud);
setTag('');
};
let scores;
if (clicked !== false) {
scores = <ul className='grades-list'>
{student.grades.map((grade, index) => <li key={index} className='grade'>Test {(index + 1) + ':'} {grade}%</li>)}
</ul>;
}
return (
<div className='studentCard' >
<div className='pic-and-text'>
<img className='student-image' alt='' src={student.pic}></img>
<section className='right-side'>
<h3 id='names'>{student.firstName.toUpperCase() + ' ' + student.lastName.toUpperCase()}</h3>
<h4 className='indent'>Email: {student.email}</h4>
<h4 className='indent'>Company: {student.company}</h4>
<h4 className='indent'>Skill: {student.skill}</h4>
<h4 className='indent'>Average: {student.grades.reduce((a, b) => parseInt(a) + parseInt(b), 0) / student.grades.length}%</h4>
{scores}
<Tags student={student}/>
<form className='tag-form' onSubmit={onSubmit}>
<input className='tag-input' type='text' placeholder='Add a tag' onChange={onChange} value={tag}></input>
</form>
</section>
</div>
<FontAwesomeIcon icon={clicked !== false ? faMinus : faPlus} className='icon' onClick={onClick}/>
</div>
)
};
And if necessary, here is the Parent Component which is attempting to receive the updated information (the callback function I am using to fetch the information from the child component is called "upStuds") :
import React, {useState, useEffect} from 'react';
import './dashboard.css';
import {StudentCard} from '../StudentCard/studentcard';
import axios from 'axios';
export function Dashboard() {
const [students, setStudents] = useState([]);
const [search, setSearch] = useState('');
const [tagSearch, setTagSearch] = useState('');
useEffect(() => {
const options = {
method: 'GET',
url: 'https://api.hatchways.io/assessment/students'
};
var index = 0;
function genID() {
const result = index;
index += 1;
return result;
};
axios.request(options).then((res) => {
const students = res.data.students;
const newData = students.map((data) => {
const temp = data;
temp["tags"] = [];
temp["id"] = genID();
return temp;
});
setStudents(newData);
}).catch((err) => {
console.log(err);
});
}, []);
const onSearchChange = (e) => {
setSearch(e.target.value);
};
const onTagChange = (e) => {
setTagSearch(e.target.value);
};
// here is the callback function that is not receiving the necessary information on time
const upStuds = (update) => {
let updatedCopy = students;
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
};
// const filteredTagged = tagList.filter
return (
<div className='dashboard'>
<input className='form-text1' type='text' placeholder='Search by name' onChange={onSearchChange}></input>
<input className='form-text2' type='text' placeholder='Search by tag' onChange={onTagChange}></input>
{students.filter((entry) => {
const fullName = entry.firstName + entry.lastName;
const fullNameWSpace = entry.firstName + ' ' + entry.lastName;
if (search === '') {
return entry;
} else if (entry.firstName.toLowerCase().includes(search.toLowerCase()) || entry.lastName.toLowerCase().includes(search.toLowerCase())
|| fullName.toLowerCase().includes(search.toLowerCase()) || fullNameWSpace.toLowerCase().includes(search.toLowerCase())) {
return entry;
}
}).map((entry, index) => {
return (<StudentCard student={entry} key={index} upStuds={upStuds} />)
})}
</div>
)
};
Please let me know if I need to clarify anything! Thanks for any assistance!
setNewStud({...student});
upStuds(newStud);
If you want to send the new state to upStuds, you can assign it to a variable and use it twice:
const newState = {...student};
setNewStud(newState);
upStuds(newState);
Additionally, you will need to change your upStuds function. It is currently mutating the existing students array, and so no render will occur when you setStudents. You need to copy the array and edit the copy.
const upStuds = (update) => {
let updatedCopy = [...students]; // <--- using spread operator to create a shallow copy
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
}
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 am kinda reposting a question i posted earlier since i think that i didn't ask it well!
so i am using axios to make HTTP requests to a json-server api. all these requests are put inside a file i called "persons.js".
import axios from "axios";
const URL = "http://localhost:3001/persons";
const getPersons = () => {
return axios.get(URL).then((res) => res.data);
};
const addPerson = (person) => axios.post(URL, person);
// this is where i have the probelm (i think)
const updatePerson = (person, number, setErrMsg) => {
axios
.put(`${URL}/${person[0].id}`, {
name: person[0].name,
number,
})
//i wanted the change the state of the App.js from this line after there is an error
.catch((err) => setErrMsg("err"));
};
const deletePerson = (person) => axios.delete(`${URL}/${person.id}`);
export { getPersons, addPerson, deletePerson, updatePerson };
and i have this feature where -everytime the user add or update a person's phone- there is a component called "PersonNotification" that render an h1 element that say either "the phone was added" or "updated".
but i want to have another "notification" that say "this phone doesn't exist anymore" if the phone -that the user wanted to update- gets deleted
here is the PersonNotification component
import React from "react";
import "./PersonNotification.css";
const PersonNotification = ({ notification, errMsg }) => {
if (errMsg.length > 0) {
return <h1 className="err">{notification}</h1>;
}
if (notification.length === 0) {
return <></>;
} else {
return <h1 className="notification">{notification}</h1>;
}
};
export default PersonNotification;
now; the way i'm doing this (rendering the error message that the user has been deleted) is with the promise error that appear in the console when the user try to update a non existing phone
so in the function updatePerson that is in the persons.js file
const updatePerson = (person, number, setErrMsg) => {
axios
.put(`${URL}/${person[0].id}`, {
name: person[0].name,
number,
}).catch((err) => setErrMsg("err"));
};
there's the third argument that i called "setErrMsg", this sets the state ErrMsg that is in the App.js file, when there is an error (that the phone doesn't exist anymore)
but the problem i have the this "setErrMsg" gets exucuted late or something (i checked it with the react dev tool).
here is (some of)the App.js code
import React, { useState, useEffect } from "react";
import {
getPersons,
addPerson,
deletePerson,
updatePerson,
} from "./services/persons";
import Filter from "./components/Filter";
import Form from "./components/Form";
import Phonebook from "./components/Phonebook";
import PersonNotification from "./components/PersonNotification";
const App = () => {
const [persons, setPersons] = useState([]);
const [newName, setNewName] = useState("");
const [newNumber, setNewNumber] = useState("");
const [query, setQuery] = useState("");
const [searchValue, setSearchValue] = useState([]);
const [notification, setNotification] = useState("");
const [errMsg, setErrMsg] = useState("");
// fetching the data from json-server (i,e: db.json)
useEffect(() => {
getPersons().then((res) => setPersons(res));
}, []);
// function that fires after the submit
const personsAdder = (e) => {
e.preventDefault();
const personsObject = { name: newName, number: newNumber };
//checking if the name exists
const nameChecker = persons.filter(
(person) => person.name === personsObject.name
);
console.log(errMsg);
if (nameChecker.length > 0) {
const X = window.confirm(
`${personsObject.name} already exists do you want to update the number`
);
if (X === true) {
// updating the number if the user confirmed
updatePerson(nameChecker, newNumber, setErrMsg);
const personsCopy = persons;
const index = personsCopy.indexOf(nameChecker[0]);
personsCopy[index] = {
id: personsCopy[index].id,
name: personsCopy[index].name,
number: newNumber,
};
setPersons([...personsCopy]);
setNewName("");
setNewNumber("");
//the function the shows the notification for 5 seconds after the content was updated
const notificationSetter = () => {
let X = "";
if (errMsg.length > 0) {
X = `you can't update${nameChecker[0].name} because it doesn't exist anymore`;
} else {
X = `${nameChecker[0].name} was updated`;
}
setNotification(X);
setTimeout(() => {
setNotification("");
setErrMsg("");
}, 5000);
};
notificationSetter();
}
} else {
//adding a new user if the name was not already in the phonebook
setPersons(persons.concat(personsObject));
addPerson(personsObject);
setNewName("");
setNewNumber("");
//the function the shows the notification for 5 seconds after the content was added
const notificationSetter = () => {
setNotification(`${personsObject.name} was added`);
setTimeout(() => {
setNotification("");
}, 5000);
};
notificationSetter();
}
};
//...
i want to know why is "setErrMsg" gets exucuted late!, and what is a better way to have the notification that says "the phone doesn't exist anymore"?
Following is the data useEffect is returning in console log:
{
"sql": {
"external": false,
"sql": [
"SELECT\n date_trunc('day', (\"line_items\".created_at::timestamptz AT TIME ZONE 'UTC')) \"line_items__created_at_day\", count(\"line_items\".id) \"line_items__count\"\n FROM\n public.line_items AS \"line_items\"\n GROUP BY 1 ORDER BY 1 ASC LIMIT 10000",
[]
],
"timeDimensionAlias": "line_items__created_at_day",
"timeDimensionField": "LineItems.createdAt",
"order": {
"LineItems.createdAt": "asc"
}
I want to be able to render the above in my react app.
const ChartRenderer = ({ vizState }) => {
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => console.log(JSON.stringify(data, null, 4)))
}},[u]);
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You would have to use a state variable to persist data and update it whenever the API returns some data. In a functional component, you can use the useState hook for this purpose.
const ChartRenderer = ({ vizState }) => {
// useState takes in an initial state value, you can keep it {} or null as per your use-case.
const [response, setResponse] = useState({});
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => {
// You can modify the data here before setting it to state.
setResponse(data);
})
}},[u]);
// Use 'response' here or pass it to renderChart
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You can read more about useState in the official documentation here.
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];
}