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.
Related
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
I am using mobx-react-lite with hooks.
I have two store.
AuthStore
SomeOtherStore
This is my dummy AuthStore
import { observable, decorate, action } from 'mobx';
import { createContext } from 'react';
import { ROLE_LOGISTICS_MANAGER } from '../settings/constants';
import AuthService from '../services/AuthService';
class AuthStore {
constructor() {
this.authService = new AuthService();
}
currentMode = ROLE_LOGISTICS_MANAGER;
authenticating = true;
isLoggedIn = false;
userId = null;
loginLoading = false;
login = async params => {
this.loginLoading = true;
try {
const data = await this.authService.loginAsync(params);
this.loginLoading = false;
this.isLoggedIn = true;
} catch (e) {
console.error(e);
this.loginError = e;
} finally {
this.loginLoading = false;
}
};
}
decorate(AuthStore, {
currentMode: observable,
loginLoading: observable,
isLoggedIn: observable,
authenticating: observable,
userId: observable,
fetchUser: action,
login: action
});
export const AuthStoreContext = createContext(new AuthStore());
Now Lets say I want to change isLoggedIn from another store,
How can I do that? I tried to find ways in docs, couldn't find a solid solution.
I am using hooks with mobx-react-lite
So normally I use mobx like
const authStore = useContext(AuthStoreContext);
It's a common pattern to have stores as properties on a RootStore, each having references back to the root. So you could have a structure like:
class RootStore {
constructor (auth, ui) {
this.auth = new AuthStore(this)
this.ui = new UiStore(this)
}
}
class AuthStore {
constructor (rootStore) {
this.rootStore = rootStore
}
logout() {
this.isLoggedIn = false
}
}
decorate(AuthStore, {
logout: action
})
Then, when you need to call a function on another store, you can use the reference to the root as a pathway. The pattern's described in more detail here. A possible example of use with useContext might be:
const { someStore } = useContext(rootStoreContext)
someStore.rootStore.auth.logout()
I have implemented a component (for a typing training app) which tracks key presses on global scope like this:
class TrainerApp extends React.Component {
constructor() {
// ...
this.handlePress = this.handlePress.bind(this);
}
handlePress(event) {
const pressedKey = event.key;
const task = this.state.task;
const expectedKey = task.line[task.position];
const pressedCorrectly = pressedKey == expectedKey;
this.setState(prevState => {
const newPosition = prevState.task.position +
(pressedCorrectly ? 1 : 0);
return {
// ...prevState, not needed: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-are-merged
task: {
...prevState.task,
position: newPosition,
mistakeAtCurrentPosition: !pressedCorrectly
}
}
})
}
componentDidMount() {
document.addEventListener(this.keyEventTypeToHandle,this.handlePress);
}
componentWillUnmount () {
document.removeEventListener(this.keyEventTypeToHandle,this.handlePress);
}
...
}
and I'd like to write some unit-tests using Jest. My initial idea was:
describe('TrainerApp.handlePress should',() => {
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
...
});
but the problem is app.handlePress relies on usage of this.setState which is not defined when the component is not mounted yet. Of'course I can modify the app like this:
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.setState = jest.fn(function(handler) {
this.state = handler(this.state);
});
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
or even like this:
class ExplorableTrainerApp extends TrainerApp {
setState(handler) {
this.state = handler(this.state);
}
}
test('move to the next char on correct press',() => {
const app = new ExplorableTrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
but this seems a very fragile approach (here I rely on the fact that .setState is called with the function argument while it can be called with just newState argument and hence I'm testing implementation details, instead of just the behaviour. Is there a more robust way to test this?
There are a few frameworks for testing React components, Enzyme and react-testing-library are both popular and well supported.
I have simple component that renders null, but should show iOS/Android alert when shown prop from notification store is changed to true, it works fine just one time using autorun / reaction / when from mobx and I can see through spy that hideNotification is also being fired correctly to set shown back to false, yet I can't re-trigger alert anymore.
Component
import { Component } from "react";
import { observer, inject } from "mobx-react/native";
import { reaction } from "mobx";
import { Alert } from "react-native";
#inject("notification")
#observer
class Notification extends Component {
// -- methods ------------------------------------------------------------- //
componentDidMount() {
reaction(
() => this.props.notification.shown,
() => {
if (this.props.notification.shown)
Alert.alert(
this.props.notification.type,
this.props.notification.message,
[
{
text: "Close",
onPress: this.props.notification.hideNotification
}
]
);
}
);
}
// -- render -------------------------------------------------------------- //
render() {
return null;
}
}
export default Notification;
Store
import { observable, action } from "mobx";
const ERROR = "notification/ERROR";
const SUCCESS = "notification/SUCCESS";
class NotificationStore {
// -- store --------------------------------------------------------------- //
#observable type = ERROR;
#observable message = "";
#observable shown = false;
// -- actions ------------------------------------------------------------- //
#action
errorNotification(message) {
this.type = ERROR;
this.message = message;
this.shown = true;
}
#action
successNotification(message) {
this.type = SUCCESS;
this.message = message;
this.shown = true;
}
#action
hideNotification() {
this.shown = false;
}
}
export default NotificationStore;
And the issue was that class functions were not binded correctly changing them to
#action
errorNotification = (message) => {
this.type = ERROR;
this.message = message;
this.shown = true;
}
Solved this for me
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;