React routing without resetting DOM elements - javascript

I am working on a POC app that includes a third-party map. Using react-router, each time the user navigates to the map this one is created from scratch (loader appearing during few seconds, zoom/rotation/modal displayed/etc reseted).
We could think about storing zoom/rotation/etc in component' state and re-apply them (which the API does not permit anyway) but we especially cannot afford the map initialization time on each map display, the user experience is just awful.
Is there a way to tell react to keep a copy of the DOM tree for this component ? To not reset it ?
I tried returning false from the 'shouldComponentUpdate' function without any success.
I could also develop my own router, where component's domContainerNodes are simply set to display:none, but it totally feels like breaking React spirit.
Please, any clue is welcomed :-)
import React, { Component } from 'react';
import { browserHistory } from 'react-router'
import './Map.css';
class Map extends Component {
constructor(props) {
super(props);
console.log('Map props:');
console.log(props);
/**
* #see ../../README.md#using-global-variables
*/
this.ThirdPartyMapAPI = window.ThirdPartyMapAPI;
this.containerId = 'map-container';
this.isMapLoaded = false;
this.isMapReady = false;
this.areEventsBinded = false;
this.queuedAction = [];
this.state = {
poiId: props.routeParams.poiId,
poiType: props.routeParams.poiType,
};
}
bindEventHandlers = () => {
this.ThirdPartyMapAPI.Map.on('ready', () => {
this.isMapReady = true;
console.log('Map has successfully been loaded');
if (this.queuedAction.length > 0) {
this.queuedAction.pop()();
}
});
this.ThirdPartyMapAPI.Map.POI.on('tap', (data) => {
console.log('POI selected', data);
});
let commonErrorHandler = (error) => {
console.error('ThirdPartyMapAPI encountered an error: ', error);
};
this.ThirdPartyMapAPI.on('error', commonErrorHandler);
this.ThirdPartyMapAPI.Map.on('error', commonErrorHandler);
this.ThirdPartyMapAPI.Map.Route.on('error', commonErrorHandler);
this.areEventsBinded = true;
};
loadDataset = () => {
if (!this.areEventsBinded) {
this.bindEventHandlers();
}
if (!this.isMapLoaded && this.ThirdPartyMapAPI.load('MeL8ooso') === true) {
this.ThirdPartyMapAPI.Map.create(document.getElementById(this.containerId), { showMapTitle: false });
this.isMapLoaded = true;
}
this.applyOptions();
};
/**
* Apply options from querystring (e.g poiId to show)
*/
applyOptions = () => {
if (typeof this.state !== 'undefined') {
let action;
// Show a POI
if (typeof this.state.poiId !== 'undefined') {
let poiId = this.state.poiId;
action = () => {
this.ThirdPartyMapAPI.Map.POI.show(poiId);
/* Used to go back to POI list and test a new map display
window.setTimeout(() => {
browserHistory.push(`/list`);
}, 4000);*/
};
}
// Perform the action, or queue it if mobigeo is not ready yet
if (typeof action === 'function') {
if (this.isMapReady) {
action();
} else {
this.queuedAction.push(action);
}
}
}
};
componentDidMount() {
this.loadDataset();
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
return false;
}
render() {
return <div id={this.containerId}></div>
}
}
export default Map;

Related

How can I detect browser back button in react class component

I am trying to create a function when user hit browser back button it will run function deleteHeldResort that I created.
Here is my code for deleteHeldResort:
deleteHeldResorts(ReservedInventoryID: number | null = null, refreshHeldResorts: boolean = true) {
return new Promise((resolve, reject) => {
this.setState({heldResortsShowLoader: true});
this.reservationService.deleteHeldResorts(ReservedInventoryID)
.then(() => {
refreshHeldResorts && this.getHeldResorts();
resolve();
})
.catch((error: any) => {
this.catchHeldResortsError(error);
reject(error);
});
this.setState({
heldResortsShowLoader: false
});
});
}
Updated code base:
handleNavigateBack = useCallback(
(event) => {
// call your function here with whatever argument your code provides
this.reservationService.deleteHeldResorts(this.props.resId);
}
, []);
useEffect(() => {
document.addEventListener('popstate', this.handleNavigateBack );
return () => {
document.removeEventListener('popstate', this.handleNavigateBack)
}
}, [this.handleNavigateBack]);
Updated answer: class component based
Since you're using class components, I'm adding this update.
Inside the class component having deleteHeldResorts function, you can listen to popstate event:
Remark how bind keyword helps us keep the right this.
import React from "react";
import { render } from "react-dom";
class App extends React.Component {
constructor(props) {
super(props);
this.handleNavigateBack = this.handleNavigateBack.bind(this);
}
componentDidMount() {
document.addEventListener("popstate", this.handleNavigateBack);
}
componentWillUnmount() {
document.removeEventListener("popstate", this.handleNavigateBack);
}
handleNavigateBack(event) {
console.log("inside callback", event);
// change arguments as you want
this.deleteHeldResorts(null, false);
}
deleteHeldResorts(ReservedInventoryID = null, refreshHeldResorts = true) {
// your function goes down there content
console.log("inside deleteHeldResorts");
}
render() {
return <h2>popstate browser listener</h2>;
}
}
render(<App />, document.getElementById("root"));
Initial answer: function component based
Inside the component having deleteHeldResorts function, you can listen to popstate event:
// your-component.js
function YourComponent() {
function deleteHeldResorts(ReservedInventoryID: number | null = null,
refreshHeldResorts: boolean = true) {
// your function content
}
const handleNavigateBack = useCallback(
(event) => {
// call your function here with whatever argument your code provides
deleteHeldResorts(reservedInventoryID,refreshHeldResorts);
}
// depending on your logic, add deps to this array dependency
}, [])
useEffect(() => {
document.addEventListener('popstate', handleNavigateBack );
return () => {
document.removeEventListener('popstate', handleNavigateBack)
}
}, [handleNavigateBack])
return ( <>something dope</>);
}

How would you use conditional hooks inside a React.Component class

Per documentation, Hooks cannot be used inside class components. But there are ways with higher order components: How can I use React hooks in React classic `class` component?. However this answer provided does not address the case of hooks that get called on function invocation. Take this simple Toast hook from: https://jossmac.github.io/react-toast-notifications/. I'd like to call the hook inside of a class of form:
```
class MyClass extends React.Component {
onTapButton = () => {
if(conditionTrue){
addToast('hello world', {
appearance: 'error',
autoDismiss: true,
})
}
}
render(){ ... }
}
```
There'd be no way of calling addToast without using const { addToast } = useToasts() in the class method, which would throw error.
You can use withToastManager HOC to archive that work
Here is an example
import React, { Component } from 'react';
import { withToastManager } from 'react-toast-notifications';
class ConnectivityListener extends Component {
state = { isOnline: window ? window.navigator.onLine : false };
// NOTE: add/remove event listeners omitted for brevity
onlineCallback = () => {
this.props.toastManager.remove(this.offlineToastId);
this.offlineToastId = null;
};
offlineCallback = id => {
this.offlineToastId = id;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
const { isOnline } = this.state;
if (prevState.isOnline !== isOnline) {
return { isOnline };
}
return null;
}
componentDidUpdate(props, state, snapshot) {
if (!snapshot) return;
const { toastManager } = props;
const { isOnline } = snapshot;
const content = (
<div>
<strong>{isOnline ? 'Online' : "Offline"}</strong>
<div>
{isOnline
? 'Editing is available again'
: 'Changes you make may not be saved'}
</div>
</div>
);
const callback = isOnline
? this.onlineCallback
: this.offlineCallback;
toastManager.add(content, {
appearance: 'info',
autoDismiss: isOnline,
}, callback);
}
render() {
return null;
}
}
export default withToastManager(ConnectivityListener);
For more information you can also find here

Tracker.autorun only runs once

Here is what I am currently working with
class FooComponent extends Component {
constructor(...args) {
super(...args);
this.state = {
model: this.getModel()
};
}
componentWillUnmount() {
this._unmounted = true;
this._modelComputation && this._modelComputation.stop();
super.componentWillUnmount && super.componentWillUnmount();
}
getModel() {
const model = {};
this._modelComputation && this._modelComputation.stop();
this._modelComputation = Tracker.autorun((computation) => {
const { id } = this.props;
const data = id && Collection.findOne(id);
if (data) {
Object.assign(model, data);
!this._unmounted && this.forceUpdate();
}
});
return model;
}
...
}
Unfortunately, the reactive model does not work, and Tracker.autorun does not execute the function when the model is updated in the database. From the documentation, Collection.findOne should be reactive, right?
I am not sure what I am doing wrong. Why isn't Tracker monitoring the DB model? Why isn't the function re-evaluating Collection.findOne when the DB changes?
** Edit **
When updating the DB, I do see the collection change through meteortoys:allthings, but autorun is not re-executed.
Looking at how tracker-react implements it, I changed my code like so
class FooComponent extends Component {
constructor(...args) {
super(...args);
this.state = {
model: this.getModel()
};
}
componentWillUnmount() {
this._unmounted = true;
this._modelComputation && this._modelComputation.stop();
super.componentWillUnmount && super.componentWillUnmount();
}
getModel() {
const model = {};
this._modelComputation && this._modelComputation.stop();
this._modelComputation = Tracker.nonreactive(() => {
return Tracker.autorun((computation) => {
const { id } = this.props;
const data = id && Collection.findOne(id);
if (data) {
Object.assign(model, data);
!this._unmounted && this.forceUpdate();
}
});
});
return model;
}
...
}
I don't fully understand why, but it works, now.

Persistent bridge between a React VR component and native module code

I am trying to have my browser send in some DOM events into the React VR components.
The closest I got is this code using "native modules."
(client.js)
const windowEventsModule = new WindowEventsModule();
function init(bundle, parent, options) {
const vr = new VRInstance(bundle, 'WelcomeToVR', parent, {
...options,
nativeModules: [windowEventsModule]
});
windowEventsModule.init(vr.rootView.context);
vr.start();
return vr;
}
window.ReactVR = {init};
(WindowEventsModule.js)
export default class WindowEventsModule extends Module {
constructor() {
super('WindowEventsModule');
this.listeners = {};
window.onmousewheel = event => {
this._emit('onmousewheel', event);
};
}
init(rnctx) {
this._rnctx = rnctx;
}
_emit(name, ob) {
if (!this._rnctx) {
return;
}
Object.keys(this.listeners).forEach(key => {
this._rnctx.invokeCallback(this.listeners[key], [ob]);
});
}
onMouseWheel(listener) {
const key = String(Math.random());
this.listeners[key] = listener;
return () => {
delete this.listeners[key];
};
}
}
So my components can now call WindowEvents.onMouseWheel(function() {}), and get a callback from the DOM world.
Unfortunately, this only works once. RN will apparently invalidate my callback after it is called.
I also investigated this._rnctx.callFunction(), which can call an arbitrary function on something called "callable module". I don't see how I can get from there to my components.
Is there something I am missing? What's the pattern to feed arbitrary messages from the native world into the ReactVR background worker?
I figured it out... sort of.
The trick was to create my own "BatchedBridge", which I can then reach using the callFunction() from the context.
(index.vr.js)
import React from 'react';
import {AppRegistry, asset, Pano, View} from 'react-vr';
import BatchedBridge from 'react-native/Libraries/BatchedBridge/BatchedBridge';
import lodash from 'lodash';
class BrowserBridge {
constructor() {
this._subscribers = {};
}
subscribe(handler) {
const key = String(Math.random());
this._subscribers[key] = handler;
return () => {
delete this._subscribers[key];
};
}
notifyEvent(name, event) {
lodash.forEach(this._subscribers, handler => {
handler(name, event);
});
}
}
const browserBridge = new BrowserBridge();
BatchedBridge.registerCallableModule(BrowserBridge.name, browserBridge);
export default class WelcomeToVR extends React.Component {
constructor(props) {
super(props);
this.onBrowserEvent = this.onBrowserEvent.bind(this);
}
componentWillMount() {
this.unsubscribe = browserBridge.subscribe(this.onBrowserEvent);
}
onBrowserEvent(name, event) {
// Do the thing here
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
}
render() {
//...
}
};
AppRegistry.registerComponent('WelcomeToVR', () => WelcomeToVR);
(WindowEventsModule.js)
import {Module} from 'react-vr-web';
import lodash from 'lodash';
const eventToOb = (event) => {
const eventOb = {};
for (let key in event) {
const val = event[key];
if (!(lodash.isFunction(val) || lodash.isObject(val))) {
eventOb[key] = val;
}
}
return eventOb;
};
export default class WindowEventsModule extends Module {
constructor() {
super('WindowEventsModule');
this._bridgeName = 'BrowserBridge';
window.onmousewheel = event => {
this._emit('onmousewheel', event);
};
}
init(rnctx) {
this._rnctx = rnctx;
}
_emit(name, event) {
if (!this._rnctx) {
return;
}
const eventOb = eventToOb(event);
this._rnctx.callFunction(this._bridgeName, 'notifyEvent', [name, eventOb]);
}
}
This feels very hacky, as it doesn't seem BatchedBridge was ever meant to be exposed to consumers.
But until there is a better option, I think I'll go with this.

How to use a custom component with react-router route transitions?

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?
The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously. This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:
// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
let withinHook = false
let finalResult = undefined
let finalResultSet = false
router.setRouteLeaveHook(route, nextLocation => {
withinHook = true
if (!finalResultSet) {
hook(nextLocation).then(result => {
finalResult = result
finalResultSet = true
if (!withinHook && nextLocation) {
// Re-schedule the navigation
router.push(nextLocation)
}
})
}
let result = finalResultSet ? finalResult : false
withinHook = false
finalResult = undefined
finalResultSet = false
return result
})
}
Here is an example of how to use it, using vex to show a dialog box:
componentWillMount() {
setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave(nextLocation) {
return new Promise((resolve, reject) => {
if (!this.state.textValue) {
// No unsaved changes -- leave
resolve(true)
} else {
// Unsaved changes -- ask for confirmation
vex.dialog.confirm({
message: 'There are unsaved changes. Leave anyway?' + nextLocation,
callback: result => resolve(result)
})
}
})
}
I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted:
https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md
return false to prevent a transition w/o prompting the user
However, they forget to mention that the hook should be unregistered as well, see here and here.
We can use this to implement our own solution as follows:
class YourComponent extends Component {
constructor() {
super();
const {route} = this.props;
const {router} = this.context;
this.onCancel = this.onCancel.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.unregisterLeaveHook = router.setRouteLeaveHook(
route,
this.routerWillLeave.bind(this)
);
}
componentWillUnmount() {
this.unregisterLeaveHook();
}
routerWillLeave() {
const {hasConfirmed} = this.state;
if (!hasConfirmed) {
this.setState({showConfirmModal: true});
// Cancel route change
return false;
}
// User has confirmed. Navigate away
return true;
}
onCancel() {
this.setState({showConfirmModal: false});
}
onConfirm() {
this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
this.context.router.goBack();
}.bind(this));
}
render() {
const {showConfirmModal} = this.state;
return (
<ConfirmModal
isOpen={showConfirmModal}
onCancel={this.onCancel}
onConfirm={this.onConfirm} />
);
}
}
YourComponent.contextTypes = {
router: routerShape
};
Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter
import React, {PropTypes as T} from 'react';
...
componentWillMount() {
this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
}
routerWillLeaveCallback(nextLocation) {
let showModal = this.state.unsavedChanges;
if (showModal) {
this.setState({
openUnsavedDialog: true,
unsavedResolveCallback: Promise.resolve
});
return false;
}
return true;
}
}
YourComponent.contextTypes = {
router: T.object.isRequired
};
Above is great except when user goes back in history. Something like the following should fix the problem:
if (!withinHook && nextLocation) {
if (nextLocation.action=='POP') {
router.goBack()
} else {
router.push(nextLocation)
}
}
Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'
// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false
#withRouter
#connect(
state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
constructor(props) {
super(props)
this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
this.state = ({ unsavedFormDialog: false })
}
areThereUnsavedChanges() {
return this.props.form && Object.values(this.props.form).length > 0 &&
Object.values(this.props.form)
.findIndex(frm => (Object.values(frm)
.findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
}
render() {
const moveForward = () => {
this.setState({ unsavedFormDialog: false })
navConfirmed = true
browserHistory.push(this.state.nextLocation.pathname)
}
const onHide = () => this.setState({ unsavedFormDialog: false })
if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
if (navConfirmed || !this.areThereUnsavedChanges()) {
navConfirmed = false
return true
} else {
this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
return false
}
})
}
return (
<div>
{this.props.children}
<Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
<Modal.Header>
<Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to leave the page without saving changes to the form?
<Row>
<Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
<Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
</Row>
</Modal.Body>
</Modal>
</div>
)
}
}

Categories