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

Created on 11 Aug 2016  Â·  34Comments  Â·  Source: mobxjs/mobx

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.

Most helpful comment

@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

All 34 comments

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 :)

@mweststrate Thanks for your reply. Unfortunately it doesn't work for me. RowRenderer is a function that gets called with the rowData for every element in the underlying DataSource. So my code looks like this:

<ListView
  renderRow={rowData => (
    <MyRow data={rowData} .../>
  )}/>

I've modified MyRow to be an observer but it doesn't make a difference.

https://facebook.github.io/react-native/docs/listview.html

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

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.

@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. :confused:

Hello, @winterbe and @danieldunderfelt could anyone show me example how to use mobx with react native.

Nothing special about Mobx with ReactNative. Just make sure to import
observer/native from mobx-react. And you have to call slice on observable
arrays before passing to List view data source.

Here's a starting guide:

https://medium.com/@dabit3/react-native-with-mobx-getting-started-ba7e18d8ff44#.uge82y49s

Am Sonntag, 21. August 2016 schrieb Abdulaziz Alkharashi :

Hello, @winterbe https://github.com/winterbe and @danieldunderfelt
https://github.com/danieldunderfelt could anyone show me example how to
use mobx with react native.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/476#issuecomment-241255418, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAdmqR3ADn1jtw8Lf9xuykH33PXbRcgTks5qiEi7gaJpZM4JiRNL
.

@winterbe So which way did you go? computed or slice? and why are you looking for a replacement for Redux? I'm also evaluating the same.

I use compute if the array presented in ListView has to be filtered first.
Otherwise I just call slice on the observable array before constructing the
ListView dataSource.

Am Donnerstag, 1. September 2016 schrieb Ori Harel :

So which way did you go? computed or slice?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/476#issuecomment-244199456, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAdmqScY7cbteFrbbqSpg_A8VQM21eRqks5qlzNOgaJpZM4JiRNL
.

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()))),

@danieldunderfelt can you share some code? the following doesn't work
ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 != r2 }); @computed get list() { return this.ds.cloneWithRows(this._list); }
<ListView dataSource={listStore.list} />

@sonayen did you try wrapping it in mobx.toJS?

@ajma tried dataSource={toJS(listStore.list)} and cloneWithRows(toJS(this._list));, both didn't work

Here is my full attempt:

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

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

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

  ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
  @computed get dataSource() { return this.ds.cloneWithRows(this.list); }
}

const listStore = new ListStore();

@observer class List extends Component {
  /*
  list = [
    { text: 'Hello World!' },
    { text: 'Hello React Native!' },
    { text: 'Hello MobX!' }
  ];

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

  state = { dataSource: this.ds.cloneWithRows(this.list) };
  */

  render() {
    return (
      <ListView
        dataSource={listStore.dataSource}
        renderRow={row => <Text>{row.text}</Text>}
        enableEmptySections={true}
      />
    );
  }
}

export default List;

@ajma any thoughts?

@sonayen Hi! You need to slice() the observable array before giving it to cloneWithRows. Otherwise your code above looks good!

@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

@sonayen would you mind documenting the problem and solution approach here? https://github.com/mobxjs/mobx/blob/gh-pages/docs/best/pitfalls.md I think that would be really useful for people running into this in the future!

@mweststrate sure thing, i will work on it

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?

@feroult it seems that iteration is the solution for now (es5 compatibility related).

one down side also would be that you have to explicitly iterate through each object key that you wish to have its value observed:

@observable list = [{ text: 'foo', subtext: 'bar' }];
@computed get dataSource() { this.list.forEach(e => [ e.text, e.subtext ]); // ..

more discussion about this here.

@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.

For an explanation, see also: https://github.com/mobxjs/mobx/blob/gh-pages/docs/best/react.md#mobx-only-tracks-synchronously-accessed-data

I just wonder why is this closed? Is it wont fix? Is it fixed? Could it be that ListView.DataSource iterates the array using a for loop? If that is the case, I think we can fix it very easily just by making properties of observableArray enumerable. Am I missing something or is everyone missing the most obvious solution? @winterbe @mweststrate

@danieldunderfelt Extracting the component and having a change in the mobx observable array does not seem to refresh the ListView for me...

Here is what I got:

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

@observer
export default class Movies extends Component {
    @computed get dataSource() {
        return dataSource.cloneWithRows(MovieStore.moviesList.slice());
    }

    render() {
        return (
            <View>
                <ListView
                  renderRow={(row) => <Row data={row} />}
                  dataSource={this.dataSource}
                  />
            </View>
       )
    }
}

Extracted row component:

@observer
export default class Row extends Component {
    render() {
      const movie = this.props.data;

      return (
        <View key={movie.id}>
            <View>
              <MovieItem movie={movie} />
            </View>
        </View>
      )
    }
}

A movie in the initial MovieStore.moviesList has many attributes that may change and the ListView never updates to reflect that change. For example, if a movie in the loaded list has a rating of 0, and it was changed to 5 in the MovieStore.moviesList, the ListView still displays 0, instead of refreshing to reflect 5.

I believe I implemented what you meant above for the ListView to reload on change of an attribute. Since the movie's attribute in MovieStore.moviesList changed, and that is the dataSource, with an extracted row component, why is it not reloading?

@MovingGifts You need to also decorate your Row component with @observer. As a rule of thumb, decorate all your components with @observer by default!

Also, you might want to do some changes to your DataSource. Right now, I don't believe that rowHasChanged is ever called, and if it would be called it would always return true as you're comparing objects. To have it be called when you clone, you need to always clone the previous clone of the DataSource! The simplest way to do this is to put dataSource as a property on the component class and always reassign it when you clone a new DataSource, that you of course clone from the dataSource property.

To have the function actually DO anything, I recommend comparing the id properties of the objects: rowHasChanged: ( r1, r2 ) => r1.id !== r2.id, of course assuming that they always have that id property.

I hope that makes sense! The DataSource is a bit finicky.

@danieldunderfelt Thank you so much for getting back so soon. I updated the code above to have an @observer on the row component, and ( r1, r2) => r1.id !== r2.id but still the changes don't reflect.

I think the only thing missing is what you said here:

To have it be called when you clone, you need to always clone the previous clone of the DataSource! The simplest way to do this is to put dataSource as a property on the component class and always reassign it when you clone a new DataSource, that you of course clone from the dataSource property.

Do you mind sharing what that code looks like based on my code above, as I am not sure 100% what's left to implement to get it to work?

@danieldunderfelt Do you mind sharing what that code looks like based on my code above, as I am not sure 100% what's left to implement to get it to work?

@MovingGifts

Can you confirm that the movie objects inside the MovieStore.moviesList are also observables?

If this is the case, they should be triggering the render of the Row component when their properties are changed.

@feroult Yup, they are observable @observable moviesList = []; and then they get populated after a server call for the movies list. I am not sure if this is the missing thing from my code above that @danieldunderfelt suggested:

To have it be called when you clone, you need to always clone the previous clone of the DataSource! The simplest way to do this is to put dataSource as a property on the component class and always reassign it when you clone a new DataSource, that you of course clone from the dataSource property.

@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.

@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!

Hey guys!
I was using FlatList from React-Native as it is more efficient in handling a large number of rows.
I also was going through the process of understanding MobX, so I decided to create this Todo list app.
My code is as shown
---Store----

/**
 * Created by d3fkon on 25/6/17.
 */
import { observable, action, computed } from 'mobx';

class DataStore {
    @observable todoList = [{value: "Welcome", checked: false, id: 0}, {value: "MLG", checked: false, id: 1}];
    i = 2;
    @action createTodo = (todo) => this.todoList.push({value: todo, checked: false, id: this.i++});

    get getList() {
        console.log(this.todoList.slice());
        return this.todoList.slice();
    }
}
export default new DataStore();

---Todo Component----

/**
 * Created by d3fkon on 25/6/17.
 */
import React, { Component } from 'react';
import {
    View,
    FlatList
} from 'react-native';
import {
    ListItem,
    CheckBox,
    H2,
    Input,
    Item,
    Label,
    Button,
    Text
} from 'native-base';
import { observer } from 'mobx-react/native';
@observer
export default class Home extends Component {

    _renderListItem = (item) => {
        console.log(item);
        return (
            <ListItem>
                <Text>{item.value}</Text>
            </ListItem>
        )
    };
    render() {
        let { Store } = this.props.screenProps;
        return(
            <FlatList
                ListHeaderComponent={<ListHeader Store={Store}/>}
                data={Store.getList}
                renderItem={({item}) => this._renderListItem(item)}
                keyExtractor={(item, id) => item.id}
            />
        )
    }
}
@observer
class ListHeader extends Component {
    constructor() {
        super();
        this.state = {
            todoInput: ""
        }
    }
    addTodo() {
        let { Store } = this.props;
        console.log(Store.getList);
        Store.createTodo(this.state.todoInput);
    }
    render() {
        return(
            <View style={{flex: 8, padding: 10, flexDirection: 'row'}}>
                <Item floatingLabel style={{flex: 7}}>
                    <Label>New Todo</Label>
                    <Input onChange={(todo) => this.setState({todoInput: todo})}/>
                </Item>
                <Button style={{flex: 1, justifyContent: 'center'}} onPress={this.addTodo.bind(this)}><Text>Add</Text></Button>
            </View>
        )
    }
}

I get this error each time I add a new Todo.

Objects are not valid as a React child (found: object with keys {dispatchConfig, _targetInst, isDefaultPrevented, isPropagationStopped, _dispatchListeners, _dispatchInstances, nativeEvent, type, target, currentTarget, eventPhase, bubbles, cancelable, timeStamp, defaultPrevented, isTrusted}). If you meant to render a collection of children, use an array instead.
I believe that it is because my data prop inside my FlatList component is not receiving an array., even after using this.todoList.slice() which seems odd to me.
Any help is appreciated. Thanks!

Hi,
I am sorry but I have been struggling with List view rendering

class ProductStore {
@observable featureproducts = [];
ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });

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

@action fetchFeatureProducts() {
    var url = Config.API_BASE_URL + 'api/v1/product/featureproducts';
    return API.getFeatureProdcutService().then((resp) => {
      if (resp.status) {
        this.setFeatureProduct(resp);
      }
    }).catch((err) => {
      console.log('error',err);
    });
  }


  @action setFeatureProduct(productData) {
    this.featureproducts = productData.data.data;
  }
}

Now for the component

`@inject( "productStore", "app", "routerActions")
@observer
class HomePage extends Component {
componentDidMount() {
this.props.productStore.fetchFeatureProducts()

}
render() {

removeClippedSubviews={false}
bounces={false}
directionalLockEnabled={false}
horizontal={true}
showsHorizontalScrollIndicator={false}
dataArray={this.props.productStore.dataSource}
renderRow={item =>
imageStyle={styles.bannerSliderImage}
onPress={() => navigation.navigate("ProductList")}
bannerImageText={item.image}
bannerImageSource={item.image}
bannerSmallText={'item.bannerSmallText'}
/>}
/>}
}`

I am using nativebase list which is using listview internally not sure why all the stuff doesn work

Also I have kept
`constructor(props) {
super(props);

const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
    DataSource: ds.cloneWithRows(this.props.productStore.featureproducts.slice())
}`

not sure where should I call this.setsate to update changes plz help

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cafreeman picture cafreeman  Â·  4Comments

Niryo picture Niryo  Â·  3Comments

mehdi-cit picture mehdi-cit  Â·  3Comments

kmalakoff picture kmalakoff  Â·  4Comments

etinif picture etinif  Â·  3Comments