Uploaded image for project: 'Realm JavaScript SDK'
  1. Realm JavaScript SDK
  2. RJS-378

Change listeners on Results should always update React Native UI

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: None
    • Component/s: React Native

      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

            Assignee:
            Unassigned Unassigned
            Reporter:
            kraen.hansen@mongodb.com Kræn Hansen
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: