I know this is an over asked question regarding useEffect and useState infinite loops. I have read almost every question about it and also searched over the internet in order to try to fix it before posting here. The most recent article that I read was this one.
Before I read the article my useState function inside useEffect was without the useState's callback, so it was what I thought which was causing the problem. But after moving it from this:
setNotifications({
...notifications,
[type]: {
...notifications[type],
active: portfolioSettings[type],
payload: {
...notifications[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
});
to this:
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
payload: {
...currState[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
}));
Unfortunately the infinite loop persists. Here are all useEffect functions (the last one is the one causing the infinite looping) that I'm using in this component.
// Set default selected portfolio to the active portfolio
useEffect(() => {
let mounted = true;
const setPortfolioData = () => {
if (activePortfolio) {
const portfolioId = activePortfolio.id;
const portfolioName = activePortfolio.name;
setSelectedPortfolio({
portfolioId,
portfolioName,
});
}
};
if (mounted) setPortfolioData();
return () => {
mounted = false;
};
}, [activePortfolio]);
// Set all the categories if no category is set
useEffect(() => {
let mounted = true;
if (mounted && allCvmCategories) {
const allCvmCategoriesNames = allCvmCategories.map((c) => c);
setNotifications((currState) => ({
...currState,
isCvmNotificationEnabled: {
...currState.isCvmNotificationEnabled,
payload: {
...currState.isCvmNotificationEnabled.payload,
cvmCategories: allCvmCategoriesNames,
},
},
}));
setIsCvmCategoriesReady(true);
}
return () => {
mounted = false;
};
}, [allCvmCategories]);
// THE ONE WHICH IS CAUSING INFINITE LOOPING
// THE notificationsTypes AND notificationsInitialState
// ARE DECLARED IN THE ROOT OF THE JSX FILE
// OUT OF THE FUNCTIONAL COMPONENT
useEffect(() => {
let mounted = true;
if (
mounted &&
isCvmCategoriesReady &&
allPortfolios &&
selectedPortfolio.portfolioId
) {
const { portfolioId } = selectedPortfolio;
const portfolioSettings = allPortfolios[portfolioId].settings;
if (portfolioSettings) {
notificationsTypes.forEach((type) => {
if (Object.prototype.hasOwnProperty.call(portfolioSettings, type)) {
const { payloadKeys } = notificationsInitialState[type];
if (payloadKeys.length > 0) {
payloadKeys.forEach((pKey) => {
if (
Object.prototype.hasOwnProperty.call(portfolioSettings, pKey)
) {
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
payload: {
...currState[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
}));
}
});
} else {
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
},
}));
}
}
});
}
}
return () => {
mounted = false;
};
}, [allPortfolios, isCvmCategoriesReady, selectedPortfolio]);
Thanks for the responses. I have found the problem, it was with the hook that I was using (SWR) for fetching. Making the allPortfolios change all the time. I have fixed it wrapping into a new custom hook.
Related
I don't understand why below code doesn't change state. Even when 'if' statement is executed, state is the same. Why case with if doesn't change state?
class Welcome extends React.Component {
state = {
items: [
{
id: 1,
done: false,
},
{
id: 2,
done: false,
},
{
id: 3,
done: false,
},
]
}
handleDone = (index) => {
this.setState((prevState) => {
const copyItems = [...prevState.items];
if (copyItems[index].done === false) {
console.log("Done should be true");
copyItems[index].done = true;
} else {
console.log("Done should be false");
copyItems[index].done = false;
}
// copyItems[index].done = !copyItems[index].done - the same result
return {
items: [...copyItems],
};
});
}
render() {
return (
this.state.items.map((item, index) => {
return (
<div>
<span>id: {item.id}</span>
<span> {item.done ? "- is not done" : "- is done"} </span>
<button
onClick={() => this.handleDone(index)}>
Change to opposite
</button>
</div>
)
})
)
}
}
ReactDOM.render(<Welcome />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
this.setState((prevState) => {
const copyItems = [...prevState.items];
if (copyItems[index].done === false) {
console.log("Done should be true");
copyItems[index].done = true;
} else {
console.log("Done should be false");
copyItems[index].done = false;
}
// copyItems[index].done = !copyItems[index].done - the same result
return {
items: [...copyItems],
};
});
Below example works fine when there is no if statement:
this.setState((prevState) => {
const copyItems = [...prevState.items];
copyItems[index].done = true;
return {
items: [...copyItems],
};
});
Below example works fine with 'if' statement in case when object is copied:
this.setState((prevState) => {
const copyItems = JSON.parse(JSON.stringify([...prevState.items]));
copyItems[index].done = !copyItems[index].done
return {
items: [...copyItems],
};
});
What's wrong for React is that even if you spread your array, the object inside it are still referenced as the object inside your state. So by doing copyItems[index].done = true; you're actually mutating the state directly (this.state[index].done should be true also). What you can do to avoid this is to use .map on your prevState so you're not updating the state directly.
const state = [{ done: true }];
const stateCopy = [...state];
const toFind = 0;
stateCopy[toFind].done = false;
console.log(stateCopy[toFind], state[toFind]); // { done: false } , { done: false } /!\ state should not be updated at this point.
// good way of doing that might be by using prevState.items.map() and do your logic inside it
const stateUpdated = state.map((el, index) => {
if (index === toFind) {
return { done: !el.done }
}
return el;
});
console.log(stateUpdated[toFind], state[toFind]) // state is not mutated, as expected
Problem was caused by create-react-app that wrap app with <React.StrictMode>.
To resolve it just remove this wrapping tag. And that's it.
It's expected behaviour and it should help avoid bugs on prod. More details in React docs.
title pretty much says it, I've looked at some examples, but none of the fixes really worked for me. I understand it's the const, moving it within the class and out of the export, no luck. Can't seem to get it working. Anyone got any ideas? Thank you.
I get the error below at the line const [ws, setWs] = useState();
"Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app"
export default function PlayerHoc(ComposedComponent) {
const [ws, setWs] = useState();
const [roomIp, setRoomIp] = useState();
useEffect(() => {
const wsUrl =
process.env.NODE_ENV == "development"
? "ws://localhost:8888"
: "ws://" + roomIp;
setWs(new WebSocket(wsUrl));
}, [roomIp]);
useEffect(() => {
if (!ws) return;
ws.onopen = () => {
ws.send("PlaybackRequest");
};
}, [ws]);
class PlayerHoc extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.playing || (this.props.playing && !nextProps.playing);
}
componentDidUpdate(prevProps) {
if (prevProps.currentSong.id !== this.props.currentSong.id) {
const id = this.props.currentSong.id;
const other = this.props.currentSong.linked_from
? this.props.currentSong.linked_from.id
: null;
this.props.containsCurrentSong(other ? `${id},${other}` : id);
}
}
render = () => (
<ComposedComponent
{...this.props}
playContext={(context, offset) => this.props.playSong(context, offset)}
playSong={() => this.props.playSong()}
{...setRoomIp(this.props.roomIp)}
/>
);
}
//testbug
const mapStateToProps = state => {
return {
currentSong: state.playerReducer.status
? state.playerReducer.status.track_window.current_track
: {},
contains: state.libraryReducer.containsCurrent ? true : false,
trackPosition: state.playerReducer.status
? state.playerReducer.status.position
: 0,
playing: state.playerReducer.status
? !state.playerReducer.status.paused
: false
};
};
function nextSong(skip) {
ws.send(JSON.stringify({ type: "skipSong", data: skip }))
}
function previousSong(prev) {
ws.send(JSON.stringify({ type: "previousSong", data: prev }))
}
function pauseSong(pause) {
ws.send(JSON.stringify({ type: "pauseSong", data: pause }))
}
function playSong(play) {
ws.send(JSON.stringify({ type: "playSong", data: play }))
}
function seekSong(seek) {
ws.send(JSON.stringify({ type: "seekSong", data: seek }))
}
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
nextSong,
previousSong,
pauseSong,
playSong,
seekSong,
},
dispatch
);
};
return connect(mapStateToProps,mapDispatchToProps)(PlayerHoc);
}
Well, this is a mess. But let's try to refactor this into a working Higher Order Component.
There are several issues here, but the main ones are:
Defining a class component inside of a functional component
Improper use of hooks.
So lets start by defining a normal Higher Order Component. Lets call it withPlayer.
withPlayer is going to return a Class component.
Inside this class component we can do things like create a websocket, and build all of your player controls.
Then we can pass those player controls as props to the Wrapped Component.
Finally, our default export will apply our redux connect HOC. We can use the compose function from redux, to compose both withPlayer and connect onto our Wrapped Component.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
function withPlayer(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
const wsUrl =
process.env.NODE_ENV == 'development' ? 'ws://localhost:8888' : 'ws://' + props.roomIp;
this.ws = new WebSocket(wsUrl);
}
shouldComponentUpdate(nextProps) {
return nextProps.playing || (this.props.playing && !nextProps.playing);
}
componentDidUpdate(prevProps) {
if (prevProps.currentSong.id !== this.props.currentSong.id) {
const id = this.props.currentSong.id;
const other = this.props.currentSong.linked_from
? this.props.currentSong.linked_from.id
: null;
this.props.containsCurrentSong(other ? `${id},${other}` : id);
}
}
nextSong = (skip) => {
this.ws.send(JSON.stringify({ type: 'skipSong', data: skip }));
};
previousSong = (prev) => {
this.ws.send(JSON.stringify({ type: 'previousSong', data: prev }));
};
pauseSong = (pause) => {
this.ws.send(JSON.stringify({ type: 'pauseSong', data: pause }));
};
playSong = (play) => {
this.ws.send(JSON.stringify({ type: 'playSong', data: play }));
};
seekSong = (seek) => {
this.ws.send(JSON.stringify({ type: 'seekSong', data: seek }));
};
render() {
return (
<WrappedComponent
{...this.props}
playContext={(context, offset) => this.playSong(context, offset)}
nextSong={this.nextSong}
previousSong={this.previousSong}
pauseSong={this.pauseSong}
playSong={this.playSong}
seekSong={this.seekSong}
/>
);
}
};
}
const mapStateToProps = (state) => {
return {
currentSong: state.playerReducer.status
? state.playerReducer.status.track_window.current_track
: {},
contains: state.libraryReducer.containsCurrent ? true : false,
trackPosition: state.playerReducer.status ? state.playerReducer.status.position : 0,
playing: state.playerReducer.status ? !state.playerReducer.status.paused : false,
};
};
export default compose(withPlayer, connect(mapStateToProps));
This is how you would use it
import withPlayer from './withPlayer'
const MyComponent = props => {
return <>You're Player wrapped component</>
}
export default withPlayer(Mycomponent);
I have a Chat component which uses API to populate the messages state, also there are different areas that have different chats which I pass as props to the component.
In this component I have 3 useEffects but I am interested in two of them which don't work properly. In the first useEffect I have some code that basically resets the messages state on area change to undefined. I need to do this to be able to distinguish between the API not being called yet where I display a loading component <Spinner /> or if the API has been called and it has retrieved an empty array to show the <NoData> component.
The problem that I have is that when I change areas the useEffects get triggered as they should but the first useEffect doesn't update the messages state to undefined before the second useEffect is called. And after a rerender because of history push the messages come as undefined but then the second useEffect doesn't get triggered anymore. I don't get why the state is not being updated in the first useEffect before the second. Also the weird thing is this used to work for me before now it doesn't. I changed some stuff up without pushing to git and now I am puzzeled. Code below:
export default function ChatPage({ history, match, area, ...props }) {
const [templates, setTemplates] = useState([]);
const [advisors, setAdvisors] = useState([]);
const [messages, setMessages] = useState(undefined);
const [conversation, setConversation] = useState([]);
const [chatToLoad, setChatToLoad] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [linkOrigin, setLinkOrigin] = useState("");
const [headerText, setHeaderText] = useState("");
// useEffect used to reset messages and conversation state
// triggered on area change(messages and conversation reset)
// and customer ID change(conversation reset).
// Required to distinguish between API call not being made yet
// and API returning no data.
useEffect(() => {
if (match.params.id) {
setLinkOrigin(match.params.id);
}
if (messages) {
if (match.params.id && messages.length !== 0) {
let matches = messages.filter(
(message) => message.ORIGINATOR === match.params.id
);
if (matches.length !== 0 && match.params.id === linkOrigin) {
setMessages(undefined);
history.push("/chats/" + match.params.area);
}
}
}
setConversation([]);
}, [area, match.params.id]);
// API calls
useEffect(() => {
if (templates.length === 0) {
api.getTemplates().then((templates) => {
setTemplates(templates);
});
}
if (advisors.length === 0) {
api.getAgents().then((advisors) => {
setAdvisors(advisors);
});
}
if (!messages || messages.length === 0) {
chooseQueue(match.params.area).then((queuesData) => {
let queues = queuesData.data.map((message) => ({
DATE_SORT: message.DATE_RECIEVED,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: "SMS_OUTBOUND",
ASSIGNED_TO: message.ASSIGNED_TO || null,
}));
setMessages(orderMessagesByDate(queues));
setChatToLoad(queues[0]);
});
}
}, [area]);
useEffect(() => {
if (messages) {
if (messages.length) {
let loadId = match.params.id ? match.params.id : messages[0].ORIGINATOR;
const params = {
MobileNumber: loadId,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: message.MSG_TYPE2.replace("MobileOriginated", "SMS"),
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
setChatToLoad(
messages.find((message) => message.ORIGINATOR === loadId)
);
history.push("/chats/" + match.params.area + "/" + loadId);
}
}
}, [messages]);
function chooseQueue(queueType) {
switch (queueType) {
case "myqueue":
setHeaderText("My chats");
return queuesApi.getMyActiveQueues(area);
case "mycompleted":
setHeaderText("My completed chats");
return queuesApi.getMyCompletedQueues();
case "queues":
setHeaderText("Chats");
return queuesApi.getQueues(area);
case "completed":
setHeaderText("Completed chats");
return queuesApi.getCompletedQueues();
default:
setHeaderText("My chats");
return queuesApi.getQueues(area);
}
}
function classifyMessage(message) {
return message.MSG_TYPE.includes("OUTBOUND") ||
message.MSG_TYPE.includes("FAULT_TEST")
? "outbound"
: "inbound";
}
async function submitMessage(message) {
var params = {
number: message.ORIGINATOR,
message: message.MESSAGE,
smssize: message.MESSAGE.length
};
await messagingApi.replyToCustomer(params).then((res) => {
if (res.data[0].RVALUE === "200") {
let extendedMsg = [...messages, message];
let extendedConversation = [...conversation, message];
setConversation([...extendedConversation]);
setMessages(orderMessagesByDate([...extendedMsg]));
}
});
}
function orderMessagesByDate(list) {
return list.sort(function(x, y) {
return new Date(y.DATE_SORT) - new Date(x.DATE_SORT);
});
}
const modalHandler = () => {
setIsOpen(!isOpen);
};
let chatConfig = {
channelSwitch: true,
channels: channels,
templateModal: true,
templates: templates,
advisorModal: true,
advisors: advisors,
};
const onActiveChatChange = (message) => {
history.push("/chats/" + match.params.area + "/" + message.ORIGINATOR);
const params = {
MobileNumber: message.ORIGINATOR,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
};
return (
<div data-test="component">
<BodyHeader
text={headerText}
children={
<FontAwesomeIcon
icon="plus-square"
aria-hidden="true"
size="2x"
onClick={modalHandler}
/>
}
/>
{messages && chatToLoad ? (
<>
<ChatWindow
messages={messages}
conversation={conversation}
chatToLoad={chatToLoad}
onActiveChatChange={onActiveChatChange}
classifyMessage={classifyMessage}
submitMessage={submitMessage}
config={chatConfig}
/>
<SendMessageModal isOpen={isOpen} toggle={modalHandler} />
</>
) : !messages ? (
<Spinner />
) : (
<NoDataHeader>There are no chats in this area</NoDataHeader>
)}
</div>
);
}
You can't get what you want this way. A state change applied in a useEffect won't have effect until the next rendering cycle, the following callbacks will still see the current const value.
If you want to change the value in the current rendering cycle the only option you have is to relax your const into let and set the variables yourself.
After all: you were expecting a const to change isn't it? ;)
retrieveInTransit() is the click handler. An API call retrieves data and returns an array of objects into the new state object in the setState() function. The first click logs an empty array for the pumps property. If I click it again, I get the populated array of pumps. Why do I have to click twice? The Equipment component does not render even though the log in the render() function returns the correct array of pumps which is passed in as a prop for the Equipment component. I am at a loss on this and any help would be appreciated.
import React, { Component } from 'react';
import {ListGroup} from 'reactstrap';
import Equipment from './Equipment';
class Transit extends Component {
constructor(props) {
super(props);
this.state = {
pending: false,
pumps: []
};
}
handleCancelClick = (index) => {
this.setState((prevState) =>
prevState.pumps[index].isCancelled = !prevState.pumps[index].isCancelled
);
this.setState((prevState) => {
prevState.pumps.every((equipment) => equipment.isCancelled === false)
? prevState.pending = false: prevState.pending = true
}
)};
populate_transit = () => {
let pumpArray = [];
fetch('http://192.168.86.26:8000/api/v1/transit_list', {mode: 'cors'})
.then(response => response.json())
.then(MyJson => MyJson.map((entry) =>{
if (document.getElementsByClassName('title')[0].innerText.toLowerCase().includes(entry.transferfrom)) {
pumpArray.push(
{
unitnumber: entry.unitnumber,
id: entry.id,
message: entry.unitnumber + ' was moved to ' + entry.transferto,
isCancelled: false
});
}}));
return pumpArray
};
cancelTransit = () => {
let cancelled = this.state.pumps.filter((movement) => movement.isCancelled);
let cancelledId = cancelled.map((object) => object.id);
fetch('http://192.168.86.26:8000/api/v1/transit_list/', {
method:'POST',
mode: 'cors',
body: JSON.stringify(cancelledId),
headers:{
'Content-Type': 'application/json'
}
}
);
this.populate_transit()
};
retrieveInTransit = () => {
if (this.state.pending) {
this.setState({
pending: false,
pumps: this.cancelTransit()
}, console.log(this.state))} else {
this.setState({
pending: false,
pumps: this.populate_transit()
}, console.log(this.state))
}
};
render() {
console.log(this.state.pumps);
return (
<ListGroup>
<Equipment transitequipment={this.state.pumps} cancelClick={this.handleCancelClick}/>
<button className='btn btn-dark' onClick={this.retrieveInTransit}>
{this.state.pending ? 'Submit Cancellations': 'Refresh'}
</button>
</ListGroup>
);
}
}
export default Transit;
populate_transit = () => {
let pumpArray = [];
fetch('http://192.168.86.26:8000/api/v1/transit_list', {mode: 'cors'})
.then(response => response.json())
.then(MyJson => MyJson.map((entry) =>{
if (document.getElementsByClassName('title')[0].innerText.toLowerCase().includes(entry.transferfrom)) {
pumpArray.push(
{
unitnumber: entry.unitnumber,
id: entry.id,
message: entry.unitnumber + ' was moved to ' + entry.transferto,
isCancelled: false
});
}}));
return pumpArray};
So, In the code above, you are returning pumpArray outside of the fetch call. Since fetch takes time to call the api and resolve the promise, the current value of your pumpArray = [] that's why you get the empty array at first. On the next click promise is already resolved so you get your pumpedArray.
In order to fix this, move the pumpArray inside then.
I am using google's autocomplete API to improve address input in my form.
I am using GoogleMapsLoader loader which dispatches action once loaded:
GoogleMapsLoader.onLoad(function() {
store.dispatch(GoogleActions.loaded());
});
In React component I have following input:
if (google.status === 'LOADED') {
inputGoogle = <div>
<label htmlFor={`${group}.google`}>Auto Complete:</label>
<input ref={(el) => this.loadAutocomplete(el)} type="text" />
</div>;
} else {
inputGoogle = '';
}
the loadAutocomplete method (not sure if it is best way of doing that) :
loadAutocomplete(ref) {
if (!this.autocomplete) {
this.search = ref;
this.autocomplete = new google.maps.places.Autocomplete(ref);
this.autocomplete.addListener('place_changed', this.onSelected);
}
},
UPDATE:
Using answer below I did following:
const GoogleReducer = (state = initialState, action) => {
switch (action.type) {
case 'GOOGLE_LOADED':
return Object.assign({}, state, {
status: 'LOADED',
connection: 'ONLINE'
});
case 'GOOGLE_OFFLINE':
return Object.assign({}, state, {
connection: 'OFFLINE'
});
case 'GOOGLE_ONLINE':
return Object.assign({}, state, {
connection: 'ONLINE'
});
default:
return state;
}
};
const GoogleActions = {
loaded: () => {
return (dispatch) => {
dispatch({
type: 'GOOGLE_LOADED',
});
};
},
onOnline: () => {
return (dispatch) => {
window.addEventListener('online', function() {
dispatch({
type: 'GOOGLE_ONLINE'
});
});
};
},
onOffline: () => {
return (dispatch) => {
window.addEventListener('offline', function() {
dispatch({
type: 'GOOGLE_OFFLINE'
});
});
};
}
};
Inside React component:
if (google.status === 'LOADED' && google.connection === 'ONLINE') {
inputGoogle = <div>
<label htmlFor={`${group}.google`}>Auto Complete:</label>
<input ref={(el) => this.loadAutocomplete(el)} name={`${group}.google`} id={`${group}.google`} type="text" onFocus={this.clearSearch}/>
</div>;
} else {
inputGoogle = <p>Auto Complete not available</p>;
}
So far works.
you can use the onLine method of the Navigator object, returns a boolean, true if online, then just add a statement in your react render.
https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine
render(){
var input = navigator.onLine ? <YOUR_FORM_COMPONENT> : null;
return(
<div>
{input}
</div>
)
}
I have been using react-detect-offline to handle displaying online/offline specific content, it handles older browsers who do not support the online event with polling and you can specify the polling URL in the options.
https://github.com/chrisbolin/react-detect-offline
First install the package
npm install react-detect-offline
Then in your component you would do something like
import { Offline, Online } from "react-detect-offline"
const MyComponent = () => {
return (
<div>
<Offline>You're offline right now. Check your connection.</Offline>
<Online>You're online right now.</Online>
</div>
);
}
navigator.onLine will return the status whether it is online or offline but it wont check internet connectivity is there or not.
Adding bit more to #StackOverMySoul. To get rid of this can refer below example.
var condition = navigator.onLine ? 'online' : 'offline';
if (condition === 'online') {
console.log('ONLINE');
fetch('https://www.google.com/', { // Check for internet connectivity
mode: 'no-cors',
})
.then(() => {
console.log('CONNECTED TO INTERNET');
}).catch(() => {
console.log('INTERNET CONNECTIVITY ISSUE');
} )
}else{
console.log('OFFLINE')
}
Why choose google.com?
The reason behind sending the get request to google.com instead of any random platform is because it has great uptime. The idea here is to always send the request to a service that is always online. If you have a server, you could create a dedicated route that can replace the google.com domain but you have to be sure that it has an amazing uptime.
Use navigator.onLine to check network connectivity. It return true if network connection is available else return false.
Also try to use navigator.connection to verify network connection status.
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
if (connection.effectiveType === 'slow-2g')
preloadVideo = false;
}
For more Network Information API
For react or typescript coders: this guy has simple and reusable solution for it
https://medium.com/#vivekjoy/usenetwork-create-a-custom-react-hook-to-detect-online-and-offline-network-status-and-get-network-4a2e12c7e58b
Works for me well.
You can create a custom hook and try to send requests each five seconds:
import { useState, useEffect } from 'react';
const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const interval = setInterval(() => {
fetch('https://www.google.com/', {
mode: 'no-cors',
})
.then(() => !isOnline && setIsOnline(true))
.catch(() => isOnline && setIsOnline(false));
}, 5000);
return () => clearInterval(interval);
}, [isOnline]);
return { isOnline };
};
export default useNetworkStatus;
And then use it like this:
import useNetworkStatus from '#/hooks/useNetworkStatus';
// ...
const { isOnline } = useNetworkStatus();
I made this react component, it uses react hooks, and blocks the screen with a wifi forbidden logo, useful for enterprise applications that require high network availability, you can test it by setting a very low timeout as 50ms.
ConnectionTester.js
import { useState, useEffect, useLayoutEffect } from 'react';
const estilo={
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
zIndex: 100,
overflow: 'hidden',
objectFit: 'fill',
}
const estiloimg={
position: 'absolute',
top:0,
left:0,
right:0,
bottom:0,
margin:'auto',
height:'50%',
width:'50%'
}
const ConnectionTester = (props) => {
const [url, setUrl] = useState('/');
const [urlPoll, setUrlPoll] = useState(15000);
const [urlTimeout, setUrlTimeout] = useState(1500);
const [isOffline, setIsOffline] = useState(false);
const [isLeavingPage, setIsLeavingPage] = useState(false);
useEffect(() => {
let controllerTimeout = 0;
const interval = setInterval(() => {
if (document.visibilityState !== 'visible') return;
const controller = new AbortController();
controllerTimeout = setTimeout(() => controller.abort(), urlTimeout);
fetch(url, {
mode: 'no-cors',
method: 'HEAD',
signal: controller.signal
})
.then((response) => {
setIsOffline(false)
clearTimeout(controllerTimeout);
})
.catch((error) => {
// console.dir(error);
setIsOffline(true)
clearTimeout(controllerTimeout);
});
}, urlPoll);
return () => {
clearTimeout(controllerTimeout);
clearInterval(interval);
// console.log('connection tester cleanup');
}
}, [isOffline, url, urlPoll, urlTimeout]);
useLayoutEffect(() => {
const handleBeforeUnload = (e) => {
setIsLeavingPage(true);
};
window.addEventListener('beforeunload', handleBeforeUnload );
}, [])
useEffect(() => {
if (props.url)
setUrl(props.url);
if (props.poll)
setUrlPoll(props.poll);
if (props.timeout)
setUrlTimeout(props.timeout);
}, [props])
return (
(isOffline && !isLeavingPage) && (
<div style={estilo}>
<img style={estiloimg} src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjIuODggMTIyLjg4Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6I2Q5MmQyNzt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPm5vLXdpZmk8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTEwMS42OCwzMi45MywzMi45MiwxMDEuNjhhNDkuMjksNDkuMjksMCwwLDAsNzcuODMtNDAuMjRoMEE0OS4zNCw0OS4zNCwwLDAsMCwxMDgsNDUuMTVhNDguODUsNDguODUsMCwwLDAtNi4zMi0xMi4yMlpNMjQsOTMuNSw5My40OSwyNEE0OS4zMSw0OS4zMSwwLDAsMCwyNCw5My41WiIvPjxwYXRoIGQ9Ik0zMC4yOSw1MkEzLDMsMCwwLDEsMjYsNTEuNjN2MGEzLDMsMCwwLDEsLjM0LTQuMjRoMEE1OS4yNyw1OS4yNywwLDAsMSw0My4yNywzN2E0OCw0OCwwLDAsMSwzNi40LjMxQTYxLDYxLDAsMCwxLDk2LjQ2LDQ3LjlhMS4yOSwxLjI5LDAsMCwxLC4xNy4xNiwzLDMsMCwwLDEsLjI3LDQuMDcsMS41NCwxLjU0LDAsMCwxLS4xNy4xOSwzLDMsMCwwLDEtNC4xNi4xOUE1NS4yMyw1NS4yMywwLDAsMCw3Ny40Nyw0M2E0MS44Niw0MS44NiwwLDAsMC0zMi4wOC0uMjdBNTMuMzgsNTMuMzgsMCwwLDAsMzAuMjksNTJaTTYxLjQ0LDc2LjA5QTYuNTksNi41OSwwLDEsMSw1Ni43Nyw3OGgwYTYuNjIsNi42MiwwLDAsMSw0LjY3LTEuOTNaTTUwLjA1LDcyLjVhMywzLDAsMCwxLTQuMTYtLjM1LDEuMzcsMS4zNywwLDAsMS0uMTYtLjE4LDMsMywwLDAsMSwuNDMtNC4wN2wuMTctLjE0YTI3LjY0LDI3LjY0LDAsMCwxLDcuMzMtNC4zMywyMS42OCwyMS42OCwwLDAsMSw3Ljg0LTEuNTIsMjEuMzUsMjEuMzUsMCwwLDEsNy44LDEuNDcsMjcuMTIsMjcuMTIsMCwwLDEsNy4zNCw0LjM2QTMsMywwLDAsMSw3Ny4wOCw3MmgwYTMsMywwLDAsMS0yLDEuMSwzLjA2LDMuMDYsMCwwLDEtMi4yMS0uNjZoMGEyMS4yNywyMS4yNywwLDAsMC01LjYyLTMuMzcsMTUuMTIsMTUuMTIsMCwwLDAtMTEuNDcsMCwyMiwyMiwwLDAsMC01LjcsMy40MVptLTkuNTYtOS43MS0uMTUuMTNhMy4wNiwzLjA2LDAsMCwxLTIuMDguNjcsMywzLDAsMCwxLTItMSwxLDEsMCwwLDEtLjE0LS4xNSwzLDMsMCwwLDEsLjM0LTQuMTYsNDUuNzgsNDUuNzgsMCwwLDEsMTIuMzYtOCwzMC43NiwzMC43NiwwLDAsMSwyNS42LjQyLDQ1Ljc0LDQ1Ljc0LDAsMCwxLDEyLjExLDguNDFsLjA4LjA3YTMuMDksMy4wOSwwLDAsMSwuODcsMiwzLDMsMCwwLDEtLjgyLDIuMTVsLS4wNy4wOGEzLDMsMCwwLDEtMiwuODcsMywzLDAsMCwxLTIuMTUtLjgxQTQwLjEzLDQwLjEzLDAsMCwwLDcyLDU2LjI4YTI0Ljc1LDI0Ljc1LDAsMCwwLTIxLS4zNSwzOS42OCwzOS42OCwwLDAsMC0xMC41LDYuODZaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNjEuNDQsMEE2MS4zMSw2MS4zMSwwLDEsMSwzOCw0LjY2LDYxLjI5LDYxLjI5LDAsMCwxLDYxLjQ0LDBabTQwLjI0LDMyLjkzTDMyLjkzLDEwMS42OEE0OS40NCw0OS40NCwwLDAsMCw4MC4zMSwxMDcsNDkuNTMsNDkuNTMsMCwwLDAsMTA3LDgwLjNhNDksNDksMCwwLDAsMy43My0xOC44NmgwYTQ4LjkzLDQ4LjkzLDAsMCwwLTkuMDgtMjguNTFaTTI0LDkzLjUsOTMuNSwyNEE0OS4zMiw0OS4zMiwwLDAsMCwyNCw5My41WiIvPjwvc3ZnPg==" alt="no-wifi-icon.svg"/>
</div>
)
)
};
export default ConnectionTester;
To use it, simply drop a component:
<ConnectionTester url="/" poll="9000" timeout="1500" />