-
Type: Bug
-
Resolution: Fixed
-
Priority: Major - P3
-
None
-
Affects Version/s: None
-
Component/s: React Native
-
3 - M (<= 1 month)
-
2655
Goals
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
- causes
-
RJS-444 Component doesn't rerender if realm object used as state.
- Closed