How to monitor network status change in react components - javascript

There are two event listeners that are apparently useful when to monitor the network status:
1. window.addEventListener('online', console.log('Online'));
2. window.addEventListener('offline', console.log('Offline'));
But I am not sure where to register and use them. When I use them within componentDidMount, there is no use because monitoring would happen only if the component is mounted. I want to monitor the network status in one place and use it across the application. For this, dispatching the network status in redux would be more helpful. But the problem is where to listen for this events.

Simplified example with class components:
// In your main App component
componentDidMount() {
window.addEventListener('online', () => this.props.setConnectivity('online'));
window.addEventListener('offline', () => this.props.setConnectivity('offline'));
// You don't need to worry about removeEventlistener if your main App component never unmounts.
}
// Action
const setConnectivity = (status) => ({
type: 'SET_CONNECTIVITY',
payload: status === 'online'
})
// Reducer
const connectivityReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_CONNECTIVITY':
return {
...state,
isOnline: action.payload
};
}
};
// To let a component know about the connectivity status, simply use the flag from state:
const mapStateToProps = (state) => ({
isOnline: state.connectivity.isOnline
});
// To react to status changes in any other component:
componentDidUpdate(prevProps) {
const { isOnline } = this.props;
if (!prevProps.isOnline && isOnline) {
// We went online
} else if (prevProp.isOnline && !isOnline) {
// We went offline
}
}

I suggest to use this very new library
Then you can use useNetworkStatus to get what you wanted, something like:
import React from 'react';
import { useNetworkStatus } from 'react-adaptive-hooks/network';
const MyComponent = () => {
const { effectiveConnectionType } = useNetworkStatus();
let media;
switch(effectiveConnectionType) {
case 'slow-2g':
media = <img src='...' alt='low resolution' />;
break;
case '2g':
media = <img src='...' alt='medium resolution' />;
break;
case '3g':
media = <img src='...' alt='high resolution' />;
break;
case '4g':
media = <video muted controls>...</video>;
break;
default:
media = <video muted controls>...</video>;
break;
}
return <div>{media}</div>;
};
effectiveConnectionType - will change if the network changes, `
navigator.connection.addEventListener('change', func) - this will also get trigger on online and on offline

Here is a working example of an online / offline status indicator in React+Redux.
The magic comes from having a component that adds an event listener for the online event in the component's componentDidMount event. Then it dispatches an action via Redux to update the online status in the store. A container component (MainContainer in this example) can map the global store state to props, and any presentational component can update in response to this property changing.
By the way, if you want to test this online indicator on this page with Chrome: run the snippet, then launch the Dev Tools (F12), then toggle the device toolbar (Ctrl+Shift+M) (or 2nd icon in the top left of the dev tools window), switch to Responsive layout, and toggle the Online / Offline state in the bar (as shown in this picture) as desired:
function rootReducer(currentState, action) {
currentState = currentState || { status: true }; // Initial State
switch (action.type) {
case 'SET_ONLINE_STATUS':
return { ...currentState, status: action.status };
default:
return currentState; // Always return the state
}
}
// Action Creators:
function setOnlineStatus(status) {
return { type: 'SET_ONLINE_STATUS', status };
}
// Create Store
var rootStore = Redux.createStore(rootReducer);
// Map state and dispatch to props
function mapStateToProps(state) {
return {
status: state.status
};
}
function mapDispatchToProps(dispatch) {
return Redux.bindActionCreators({
setOnlineStatus: setOnlineStatus
}, dispatch);
}
// Connection indicator pure functional presentational component
var ConnectionIndicator = (props) => {
return (<div>You are: {props.status ? 'online' : 'offline'}</div>);
};
var Main = React.createClass({
render: function () {
return (<div>
<ConnectionIndicator status={this.props.status} />
</div>);
}
});
var OnlineWatcher = React.createClass({
render() { return null; },
componentDidMount() {
window.addEventListener('online', () => {
this.props.setOnlineStatus(true);
})
window.addEventListener('offline', () => {
this.props.setOnlineStatus(false);
})
}
});
// Container components (Pass props into presentational component)
var MainContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(Main);
var OnlineWatcherContainer = ReactRedux.connect(mapStateToProps,
mapDispatchToProps)(OnlineWatcher);
// Top-Level Component
var App = React.createClass({
render: function () {
return (
<div>
<MainContainer />
<OnlineWatcherContainer />
</div>
);
}
});
// Render to DOM
var Provider = ReactRedux.Provider; // Injects store into context of all descendents
ReactDOM.render(
<Provider store={rootStore}>
<App />
</Provider>,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.13.0/polyfill.js"></script>
<div id="container">
<!-- Yield to React -->
</div>
NOTE: In production code, you should also unsubscribe from the online and offline events in the componentWillUnmount function.
I used this fiddle as a starting point.

Before all, you should have redux store with actions and reducers.You are able to connect every component you want to store and use this source of truth. You can find documentations and exampel here https://react-redux.js.org/introduction/quick-start
Actually you can find example of using online status Integrating Navigator.onLine into React-Redux

when using hooks, this simple solution would work https://github.com/saulpalv/useOnline
npm i #saulpalv/useonline
use like this
import type { FC } from 'react';
import { useOnline } from '#saulpalv/useonline';
export const DialogNetwork: FC = ()=> {
const isOnline = useOnline();
return (
<Dialog show={!isOnline}>
You are not online
</Dialog >
);
});

Related

Any change to redux store my causes component to re-render

I'm doing some testing on my UI and I've noticed that if any state changes in my redux store my component (shown below) re-renders and restarts with embedded video at 0. If I type in a redux-connected text field, it remounts, if a status notification hits the store, it remounts, etc.
I have no idea how to fix this and I could really use some help figuring out how to go after the bug.
tldr; How can I stop my VideoPlayer from re-rendering each time something changes in my redux store?
redux-toolkit
react
component
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm);
// renders output here in the same component
return (
...
{sourceContentFound === false ? (
<VideoPlayerDisabled />
) : (
<VideoPlayerController
title={title}
description={description}
sourceAddress={iframeURL}
author={authorName}
timeStamp={{ startTime: 0 }}
/>
)}
)
...
}
formSlice
export const dynamicFormSlice = createSlice({
name: 'dynamicForm',
initialState,
reducers: {
updateField: (state, action) => {
state = action.payload;
return state;
}
},
});
export const selectDynamicForm = createSelector(
(state: RootState): dynamicFormState => state.dynamicForm,
dynamicForm => dynamicForm
);
statusHandlerSlice
I don't think this component does anything crazy, per-say, but I have a notification appear when the video conditions are met. When it goes back down clearStatus the video player restarts.
export const statusHandlerSlice = createSlice({
name: 'statusHandler',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<IStatusObject>) => {
const { type, display, message } = action.payload;
state.status = {
...action.payload,
message: message.charAt(0).toUpperCase() + message.slice(1),
};
if (display === 'internal-only' || display === 'support-both') {
statusLogger(type, message);
}
},
clearStatus: state => {
state.status = {
type: 'success',
data: {},
message: '',
display: 'internal-only',
key: '',
};
},
},
});
export const { setStatus, clearStatus } = statusHandlerSlice.actions;
export const selectStatus = (state: RootState): IStatusObject =>
state.statusHandler.status;
Your MyComponent is re-render every time redux store state change is because you have a selector in it
You could stop this to happen by, add an equalityFn to useSelector.
You can write your own equalityFn or use some existing function from a library that supports deep comparison.
Ex: Use lodash isEqual
import { isEqual } from 'lodash';
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm, isEqual);
By default, useSelector use a shallow compare which can't detect deep changes inside your object, change to a deep comparison function like isEqual will help you to do that, but It's not recommended for all selector since there will be a performance impact.
Live Example:
I suggest either creating a custom equalFn to compare the data you're using in the current component or do not select the whole slice, maybe some properties change is unnecessary for your component. like:
const { data } = useSelector(store => store.sliceA, shallowEq);
// console.log(data) => { a: "foo", b: "bar" }
// data.b is useless but once it is changed, the component will re-render as well
return <Typography>{data.a}</Typography>
You should install React Devtools, turn on profiler, remember to check Record why each component rendered while profiling in setting to see what is causing re-rendering. sometimes custom hooks in libraries trigger re-rendering.
whyDidYouRender
is a good choice too

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

React conditional rendering with history

I have a parent component which maintains state for three 'form' components that render in sequence. It looks something like this:
<Parent>
{ renderFormBasedOnState() }
</Parent>
FormA renders, then when next is click FormB renders then FormC renders, all in the parent.
Previously I was using a React Router to do this, but the problem is, I don't want the user to be able to bookmark /formb or /formc, as that would be an invalid state.
I can do this with a switch statement, but then I lose forward / back button browser history ability - and I don't want to basically re-implement react-router in my component. What is the simplest way to go about this?
Haven't tried it for the back of the browser, but it could look something like this:
export default class tmp extends React.Component {
state = {
currentVisibleForm: 'A'
}
onBackButtonEvent = (e) => {
if(this.state.currentVisibleForm !== 'A') {
e.preventDefault();
//Go back to the previous visible form by changing the state
} else {
// Nothing to do
}
}
componentDidMount = () => {
window.onpopstate = this.onBackButtonEvent;
}
render() {
return (
<Parent>
{this.state.currentVisibleForm === 'A' &&
<FormA />
}
{this.state.currentVisibleForm === 'B' &&
<FormB />
}
{this.state.currentVisibleForm === 'C' &&
<FormC />
}
</Parent>
)
}
}
Tell me if it is of any help!
So I was able to get this working with the history api, however it may not be worth the effort to fine tune - I may revert. Managing state in two places is kind of dumb. Note this history object is the same from the application's 'Router' component, and doesn't conflict.
state = {
FormData: {},
action: 'Form_1'
}
componentWillMount() {
this.unlistenHistory = history.listen((location) => {
if (location.state) {
this.setState(() => ({
action: location.state.action
}));
}
});
history.push(undefined, {action: 'FORM_1'});
}
componentWillUnmount() {
this.unlistenHistory();
}
finishForm1 = () => {
const action = 'Form_2';
history.push(undefined, { action });
this.setState((prevState) => ({
// business stuff,
action
}));
};
renderCurrentState() {
switch(this.state.action) {
case 'FORM_1':
return <Form1 />
...
}
}
render() {
return (
<div>
{ this.renderCurrentState() }
</div>
);
}

How to update redux store for many components at the same time on click of a single button?

I have four stateful react components in a single page, they get updated simultaneously on the click of a single button.
I now want to use redux store to save the state of all the components.
I wrote react-redux connect for the first component and it works fine.
Then followed the same logic for the second component, but the store is not getting updated along with the first one.
How can I save the states of all the components at same time to store?
I think you can use this kind of structure.
The main page in which you are using the four stateful components should look like this.
class App extends React.Component {
render() {
const { commonState, updateCommonStateHandler } = this.props;
return (
<div>
<Component1 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component2 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component3 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component4 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
</div>
);
}
}
const mapStateToProps = state => {
return {
commonState: state.commonState
};
};
const mapDispatchToProps = dispatch => {
return {
updateCommonStateHandler: change => {
dispatch(() => ({
type: 'UPDATE_COMMON_STATE',
change
}));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
With the reducer
const updateCommonStateReducer = (state = {}, action) => {
const newState = extend({}, state);
if(action.type === 'UPDATE_COMMON_STATE') {
newState.commonState = newState.commonState || {};
extend(newState.commonState, action.change || {});
}
return newState;
};
So if you want to update the state from any child component, you should call the updateCommonStateHandler, which will dispatch an action to update the state.
On state update react will re-render all the components with new commonState.
PS: This is just a sample code explaining the situation, not the solution and it's
written in ES6

Categories

Resources