mobx: [ReactNative] ListView.DataSource doesn't work with ObservableArrays

Hi,

I’m currently evaluating MobX to be used with React Native. It seems that ListView.DataSource unfortunately doesn’t work natively with observable arrays from MobX. I have to create a native array via toJS() in order the get the ListView show any items.

@observer
export default class SampleList extends Component {
    render() {
        const {store} = this.props;

        const dataSource = new ListView.DataSource({
            rowHasChanged: (r1, r2) => r1.id !== r2.id
        });

        const items = toJS(store.items);    // <= conversion to native array necessary

        return (
            <ListView
                dataSource={dataSource.cloneWithRows(items)}
                renderRow={data => (...)}
            />
        );
    }
}

I’ve just started experimenting with MobX but I’m a little concerned that calling toJS() for large collections on every render could lead to performance problems.

Please correct me if I’m wrong and there’s another way of getting the DataSource to accept ObservableArrays.

I understand that observable types are a consequence of MobX and that you cannot ensure that every library works out of the box with those types. However in ReactNative ListView is such a fundamental component that I hope there’s a decent solution when using MobX.

Thanks.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 1
  • Comments: 34 (5 by maintainers)

Most upvoted comments

@danieldunderfelt finally it worked! thanks. i am sure someone will encounter the same problem in the future since this is a common use case, so i will post the full working code.

import React, { Component } from 'react';
import { ListView, Text, TouchableOpacity, View } from 'react-native';

import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react/native';

class ListStore {
  @observable list = [
    'Hello World!',
    'Hello React Native!',
    'Hello MobX!'
  ];

  ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });

  @computed get dataSource() {
    return this.ds.cloneWithRows(this.list.slice());
  }

  @action add = title => this.list.push(title);
}

const listStore = new ListStore();

@observer class List extends Component {
  render() {
    return (
      <View style={{ flex: 1, padding: 10 }}>
        <ListView
          dataSource={listStore.dataSource}
          renderRow={row => <Text>{row}</Text>}
          enableEmptySections={true}
        />

        <TouchableOpacity onPress={() => listStore.add('Lorem ipsum dolor sit amet')} style={{ bottom: 0 }}>
          <Text>listStore.add()</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

export default List;

img_2152

Hi @winterbe!

I’ve been using mobx with RN extensively, and I haven’t gotten mobx “arrays” to work with datasources either. The reason might be that mobx arrays are really objects and datasource expects an array. I always call slice on the array before feeding it to the datasource.

One trick is to create the datasource in a computed, so the computed observes the reactive array and returns a datasource when accessed. Then just have the ListView use the computed prop as its datasource.

Datasource did not work with peek either if I recall correctly. This indicates that RN is doing something with the array, more than just reading from it. This could be interesting to research, as a mobx-react-native-datasource would be a great module to have.

@feroult The reason you’re not seeing updates is that renderRow does not react to changes. observer only makes the render function re-run on changes, not any other function. You need to make sure that your row component state is used within a render function, and the easiest way to do that is to create a separate component.

Then your renderRow is simply:

renderRow(row) {
  return <RowComponent data={ row } />
}

So it is not the datasource that is your problem at all, it is your render function.

It seems that the solution above doesn’t work for inner object property updates. For instance, if we have this observable, instead of a plain string array:

@observable list = [
    { text: 'Hello World!' },
    { text: 'Hello React Native!' },
    { text: 'Hello MobX!' }
  ];

Then our renderRow method should be renderRow={row => <Text>{row.text}</Text>}.

Now if an @action update an item, like list[0].text = 'new text', the ListView won’t update.

The renderRow function won’t fire the reactions to recompute the dataSource() method. I think this is right, because inside the dataSource() we don’t touch the inner object properties.

The following hack will fire the ListView update after the item changes, but it doesn’t feel right 😃

 @computed get dataSource() {
    this.list.forEach(e => e.text); 
    return this.ds.cloneWithRows(this.list.slice());
  }

Finally, this issue tells that we need to clone and update the object in the array instead of just changing it’s properties. Which doesn’t feel right too.

Since the hack works (although it re-renders all items in the ListView), it seems that it is possible to handle it properly with mobx, right?

Does anyone have a better idea on how to observe and fire the updates by using the inner property access that happens inside the renderRow method call?

@mweststrate I don’t know if it reacts to changes because the ListView doesn’t render any rows at all when calling dataSource.cloneWithRows(items) with an ObservableArray instead of a native array. But calling slice seems to be a decent workaround, thanks for that!

@danieldunderfelt Using computed for the datasources is a great advice, thanks for that! I’m glad to hear people are already using MobX with ReactNative. I’m still evaluating if MobX could be a decent replacement for Redux in my app. ListView gave me a little trouble because of all the render* props which sometimes don’t react to state changes. I guess I haven’t understood entirely how observer actually works. 😕

If I remember correctly (not an active RN dev myself) the ListViewDataSource itself can work with observable arrays, but you need to make sure that your renderRow is an observer component. (It looks like being part of the SampleList, but actually these component callbacks have their own lifecycle. So

renderRow={RowRenderer}

//...

const RowRenderer = observer(data => {})

Should do the trick. Let me know if it doesn’t 😃

@danieldunderfelt @feroult @binchik Thank you so much for all your help guys.

The issue was the nested component MovieItem not having the @observer, once added it worked!

@MovingGifts I had the same problem. I ended up importing Observer component from mobx-react. Then modify your renderRow code to look like this:

renderRow = () => ( <Observer> {() => <MyRow />} </Observer> );

Now the rows are rerendered as observable data changes.

I ran into a problem when I had a list with section headers. After digging through some code, it turns out that ListViewDataSource when it was calculating rowIdentities on an ObservableArray, it does Objects.keys on it. RN expects that it would output the indexes of the array, but it doesn’t because it’s an Observable Array. My solution here is when I call cloneWithRowsAndSections, I have to pass in the sectionIdentities and rowIdenties myself

  dataSource: ds.cloneWithRowsAndSections(
    list,
    Object.keys(list),
    Object.keys(list).map((sectionID) => Object.keys(list[sectionID].slice()))),

Does it not react to changes in the row data, or to appending / remove items to the collection? For the latter you might need items.slice() to make sure RN recognizes as array (difference with toJS is that the first makes a cheap shallow copy, and the latter a deep copy, which shouldn’t be needed as individual rowData objects are observed by MyRow).

cc: @danieldunderfelt