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.
Related
I have a react class based component where I have defined a state as follows:
class MyReactClass extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedDataPoints: new Set()
};
}
// This method is called dynamically when there is new addition of data
storeData = (metricName, dataPoint) => {
if (this.state.selectedDataPoints.has(dataPoint)) {
this.state.selectedDataPoints.delete(dataPoint);
} else {
this.state.selectedDataPoints.add(dataPoint);
}
};
render () {
return (
<p>{this.state.selectedDataPoints}</p>
);
}
}
Note that initially, the state is an empty set, nothing is displayed.
But when the state gets populated eventually, I am facing trouble in spinning up the variable again. It is always taking as the original state which is an empty set.
If you want the component to re-render, you have to call this.setState () - function.
You can use componentshouldUpdate method to let your state reflect and should set the state using this.state({}) method.
Use this code to set state for a set:
export default class Checklist extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedDataPoints: new Set()
}
this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
}
addItem(item) {
this.setState(({ selectedDataPoints }) => ({
selectedDataPoints: new Set(selectedDataPoints).add(item)
}));
}
removeItem(item) {
this.setState(({ selectedDataPoints }) => {
const newSelectedDataPoints = new Set(selectedDataPoints);
newSelectedDataPoints.delete(item);
return {
selectedDataPoints: newSelectedDataPoints
};
});
}
getItemCheckedStatus(item) {
return this.state.checkedItems.has(item);
}
// This method is called dynamically when there is new addition of data
storeData = (metricName, dataPoint) => {
if (this.state.selectedDataPoints.has(dataPoint)) {
this.state.selectedDataPoints.removeItem(dataPoint);
} else {
this.state.selectedDataPoints.addItem(dataPoint);
}
};
render () {
return (
<p>{this.state.selectedDataPoints}</p>
);
}
}
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
How can I set the cursor position after creating an EditorState with content and some decorators on Draft.js. It always starts on the position 0.
This is what happens:
This is what I want:
Here is how I am creating the state.
constructor(props) {
super(props)
this.state = { editorState: this.getEditorState() }
}
getEditorState() {
const { reply: { channel, userAccount } } = this.props
const content = this.getEditorContent({ channel, userAccount })
const decorators = this.getEditorDecorators(channel)
return EditorState.createWithContent(content, decorators)
}
getEditorContent({ channel, userAccount }) {
const content = channel && channel.prefill(userAccount)
return ContentState.createFromText(content || '')
}
getEditorDecorators({ decorators }) {
return getDecorators(decorators || [])
}
After reading the issue 224 from Draft.js repository, I've found a static method called moveSelectionToEnd. Everything that I need to do is wrap the brand new state with this method, this way.
getEditorState() {
const { reply: { channel, userAccount } } = this.props
const content = this.getEditorContent({ channel, userAccount })
const decorators = this.getEditorDecorators(channel)
const state = EditorState.createWithContent(content, decorators)
return EditorState.moveSelectionToEnd(state)
}
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.
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;