Change listeners on Results should always update React Native UI
See original GitHub issueGoals
A callback registered as a listener for changes on a Realm JS Results
instance should always be able to trigger updates of the React Native UI.
Expected Results
From a React Native app, when registering a callback on a Results
object and calling this.setState
on a component, I would expect the UI to always update.
Actual Results
- The UI only updates 4 out of 5 times when the change listener fires.
- The bug seems to disappear if
this.setState
is the first method called in the callback. - It’s possible to trigger an update of the UI by touching / clicking the simulator.
Steps to Reproduce & Code Sample
Initialize a new React Native app:
npx react-native init Issue2655 --directory realm-js-issue-2655
Copy in the files below, install the dependencies
cd realm-js-issue-2655
npm install
Create an instance of ROS and update constants.js
.
In one terminal, start the updater (a node process changing a realm every second) - in another start the app on iOS
npm run updater
npm run ios
The app has two modes: “interval” where a timer will update the state every second and “realm” where a change listener will be registered and the bug is observed.
Use the Safari developer tools to attach to the apps JSContext and observe the issue in the timeline:
// TODO: Add images of the timeline and call-stacks when running in the two modes.
package.json
{
"name": "realm-js-issue-2655",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"lint": "eslint .",
"updater": "cd updater && node index.js"
},
"dependencies": {
"react": "16.9.0",
"react-native": "0.61.5",
"realm": "^3.5.0"
},
"devDependencies": {
"@babel/core": "^7.6.2",
"@babel/runtime": "^7.6.2",
"@react-native-community/eslint-config": "^0.0.5",
"eslint": "^6.5.1",
"metro-react-native-babel-preset": "^0.56.0"
},
"jest": {
"preset": "react-native"
}
}
App.js
import React, {Component} from 'react';
import {Button, Text, View} from 'react-native';
import Realm from 'realm';
import {schema} from './schema';
import {NICKNAME, SERVER_URL} from './constants';
const SEQUENCE = 'abcdefghijklmn';
const styles = {
mainView: {
justifyContent: 'center',
height: '100%',
padding: 10,
},
};
export default class App extends Component {
index = 1;
constructor(props) {
super(props);
this.state = {value: 'Not yet set', mode: 'interval'};
this.modeChanged(this.state.mode);
}
componentDidMount() {
// Open up the Realm
this.openRealm().then(null, err => {
console.error(`Failed to open Realm: ${err.message}`);
});
}
componentDidUpdate(_, prevState) {
const {mode} = this.state;
if (prevState.mode !== mode) {
this.modeChanged(mode);
}
}
componentWillUnmount() {
if (this.realm) {
this.realm.close();
}
}
render() {
console.log(`Rendered: ${this.state.value}`);
return (
<View style={styles.mainView}>
<Text>
Realm is "{this.realm && !this.realm.isClosed ? 'open' : 'closed'}"
</Text>
<Text>Mode is "{this.state.mode}"</Text>
<Text>Value is "{this.state.value}"</Text>
<Button
title={`Toggle mode to ${
this.state.mode === 'interval' ? 'realm' : 'interval'
}`}
onPress={this.toggleMode}
/>
</View>
);
}
ensureUser = async () => {
if (Realm.Sync.User.current) {
return Realm.Sync.User.current;
} else {
const credentials = Realm.Sync.Credentials.nickname(NICKNAME, true);
return Realm.Sync.User.login(SERVER_URL, credentials);
}
};
openRealm = async () => {
let user = await this.ensureUser();
const config = user.createConfiguration({
schema,
sync: {
url: '~/issue-2655',
fullSynchronization: true,
},
});
this.realm = new Realm(config);
this.singletons = this.realm.objects('Singleton');
};
callback = results => {
const [singleton] = results;
if (singleton) {
const {value} = singleton;
console.log(`Value changed to ${value}`);
this.setState({value}, () => {
console.log(`State was changed to ${this.state.value}`);
});
}
};
toggleMode = () => {
this.setState({
mode: this.state.mode === 'interval' ? 'realm' : 'interval',
});
};
modeChanged = mode => {
// Clear the interval if its mode is not interval
if (mode !== 'interval') {
clearInterval(this.interval);
}
// Remove the listener if the mode is not realm
if (mode !== 'realm' && this.singletons) {
this.singletons.removeListener(this.callback);
}
// Handle mode being set to interval
if (mode === 'interval') {
this.interval = setInterval(() => {
const value = SEQUENCE.substring(0, this.index);
this.callback([{value}]);
this.index++;
if (this.index > SEQUENCE.length) {
this.index = 1;
}
}, 1000);
}
// When the mode becomes "realm", add a listener with the callback
if (mode === 'realm' && this.singletons) {
console.log('Setting listeners on', this.singletons);
this.singletons.addListener(this.callback);
}
};
}
constants.js
module.exports = {
NICKNAME: 'realm-js-issue-2655',
SERVER_URL: 'https://[...].cloud.realm.io/', // Go to https://cloud.realm.io/ create an instance
};
schema.js
module.exports = {
schema: [
{
name: 'Singleton',
properties: {
value: 'string',
},
},
],
};
updater/index.js
const Realm = require('realm');
const {NICKNAME, SERVER_URL} = require('../constants');
const {schema} = require('../schema');
const SEQUENCE = 'ABCDEFGHIJKLMN';
let index = 1;
function update(realm) {
// Remove the first element of the list and insert a new at the end
realm.write(() => {
const value = SEQUENCE.substring(0, index);
console.log(`Changing value to "${value}"`);
const [singleton] = realm.objects('Singleton');
if (singleton) {
singleton.value = value;
} else {
realm.create('Singleton', {value});
}
// Increment the index
index++;
// Reset when it gets out of bounds
if (index > SEQUENCE.length) {
index = 1;
}
});
realm.syncSession.uploadAllLocalChanges().then(() => {
console.log('Done uploading!');
});
}
function login() {
if (Realm.Sync.User.current) {
return Realm.Sync.User.current;
} else {
const credentials = Realm.Sync.Credentials.nickname(NICKNAME, true);
return Realm.Sync.User.login(SERVER_URL, credentials);
}
}
async function run() {
const user = await login();
const config = user.createConfiguration({
schema,
sync: {
url: '~/issue-2655',
fullSynchronization: true,
},
});
const realm = new Realm(config);
// Start updating
setInterval(() => {
update(realm);
}, 1000);
}
run().then(null, err => {
console.error(err.stack);
process.exit(1);
});
Version of Realm and Tooling
- Realm JS SDK Version: 2.6.0
- Node or React Native: React Native (verified on iOS)
- Client OS & Version: N/A
- Which debugger for React Native: None
Issue Analytics
- State:
- Created 4 years ago
- Reactions:2
- Comments:21 (8 by maintainers)
What’s the status of this? I’m seeing this with
10.20.0-beta.5
as well.This is my solution to refresh the data and rerender the UI.