question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Change listeners on Results should always update React Native UI

See original GitHub issue

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

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:2
  • Comments:21 (8 by maintainers)

github_iconTop GitHub Comments

1reaction
cristianoccazinspcommented, Apr 18, 2022

What’s the status of this? I’m seeing this with 10.20.0-beta.5 as well.

1reaction
saravanakumargncommented, Apr 9, 2020

This is my solution to refresh the data and rerender the UI.

  const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);

   useEffect(() => {
    const taskLists: Realm.Results<TaskSchema & Realm.Object> = realm
      .objects<TaskSchema>(TaskSchema.schema.name)
      .filtered('dueDate >= $0 AND done = false', startOfDay(new Date()))
      .sorted('dueDate');
    taskLists.addListener(listener);   // Added Listener
    setTaskData(taskLists);

    return () => {
      taskLists.removeListener(listener);
    };
  }, []);

  function listener(allTasks: any, changes: any) {
    updateUI(allTasks);
  }

  function updateUI(taskItems: any) {
    setTaskData(taskItems);
    forceUpdate();
  }
Read more comments on GitHub >

github_iconTop Results From Across the Web

Realm & React Native - Best practice to implement auto ...
You can make your example reactive by subscribing to events and updating the ui when you receive a change event. Right now events...
Read more >
Working with App State and Event Listeners in React Native
AppState changes are managed within event listeners, the functions of which will need access to the latest component (and in cases, element) ...
Read more >
Performance Overview - React Native
This is where your React application lives, API calls are made, touch events are processed, etc... Updates to native-backed views are batched ...
Read more >
React to Changes - React Native SDK — Realm - MongoDB
To register a change listener for an entire realm, pass a callback function to the realm's addListener() method. Realm Database calls the listener...
Read more >
What every front-end developer should know about change ...
There's no way to trigger change detection automatically in React. Every change detection cycle starts with the call to the setState function. Angular....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found