react-native propagate changes in props through ListView and Navigator

5.6k Views Asked by At

I have the following situation. I have a parent component that contains a list of items, any of which can be drilled down into and viewed in a child component. From the child component, you should be able to change a value in the item you are looking at.

In the React web world, this would be easy to solve with the parent storing the list as state, and passing the item and a callback for changes as props to the child.

With React Native, it seems like that possibility is lost, since causing a change from the child component does not trigger a re-render until navigating away.

I've recorded a video of what this looks like. https://gfycat.com/GreenAgitatedChanticleer

Code is below.

index.ios.js

var React = require('react-native');
var {
    AppRegistry,
    Navigator
} = React;

var List = require('./list');

var listviewtest = React.createClass({
    render: function() {
        return (
            <Navigator
                initialRoute={{ component: List }}
                renderScene={(route, navigator) => {
                    return <route.component navigator={navigator} {...route.passProps} />;
                }} />

        );
    }
});

AppRegistry.registerComponent('listviewtest', () => listviewtest);

list.js

var React = require('react-native');
var _ = require('lodash');
var {
    View,
    Text,
    TouchableHighlight,
    ListView
} = React;

var Detail = require('./detail');

var List = React.createClass({
    getInitialState() {
        var LANGUAGES = [
            { id: 1, name: 'JavaScript' },
            { id: 2, name: 'Obj-C' },
            { id: 3, name: 'C#' },
            { id: 4, name: 'Swift' },
            { id: 5, name: 'Haskell' }
        ];

        var ds = new ListView.DataSource({ rowHasChanged: (a, b) => a !== b })
        return {
            languages: LANGUAGES,
            ds: ds.cloneWithRows(LANGUAGES)
        };
    },

    goToLanguage(language) {
        this.props.navigator.push({
            component: Detail,
            passProps: {
                language: language,
                changeName: this.changeName
            }
        });
    },

    changeName(id, newName) {
        var clone = _.cloneDeep(this.state.languages);
        var index = _.findIndex(clone, l => l.id === id);
        clone[index].name = newName;

        this.setState({
            languages: clone,
            ds: this.state.ds.cloneWithRows(clone)
        });
    },

    renderRow(language) {
        return (
            <TouchableHighlight onPress={this.goToLanguage.bind(this, language)}>
                <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', paddingTop: 5, paddingBottom: 5, backgroundColor: '#fff', marginBottom: 1 }}>
                    <Text style={{ marginLeft: 5, marginRight: 5 }}>{language.name}</Text>
                </View>
            </TouchableHighlight>
        );
    },

    render() {
        return (
            <View style={{ flex: 1, backgroundColor: '#ddd' }}>
                <Text style={{ marginTop: 60, marginLeft: 5, marginRight: 5, marginBottom: 10 }}>Select a language</Text>
                <ListView
                    dataSource={this.state.ds}
                    renderRow={this.renderRow} />
            </View>
        );
    }
});

module.exports = List;

detail.js

var React = require('react-native');
var {
    View,
    Text,
    TouchableHighlight
} = React;

var Detail = React.createClass({
    changeName() {
        this.props.changeName(this.props.language.id, 'Language #' + Math.round(Math.random() * 1000).toString());
    },

    goBack() {
        this.props.navigator.pop();
    },

    render() {
        return (
            <View style={{ flex: 1, backgroundColor: '#ddd', alignItems: 'center', justifyContent: 'center' }}>
                <Text>{this.props.language.name}</Text>

                <TouchableHighlight onPress={this.changeName}>
                    <Text>Click to change name</Text>
                </TouchableHighlight>

                <TouchableHighlight onPress={this.goBack}>
                    <Text>Click to go back</Text>
                </TouchableHighlight>
            </View>
        );
    }
});

module.exports = Detail;
2

There are 2 best solutions below

0
On

Turns out this behavior is intentional, at least for now. There's a discussion thread here: https://github.com/facebook/react-native/issues/795

For anyone looking for a workaround, I'm using RCTDeviceEventEmitter to pass data across Navigator. Updated code below

list.js

var React = require('react-native');
var _ = require('lodash');
var {
    View,
    Text,
    TouchableHighlight,
    ListView
} = React;

var Detail = require('./detail');
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

var List = React.createClass({
    getInitialState() {
        var LANGUAGES = [
            { id: 1, name: 'JavaScript' },
            { id: 2, name: 'Obj-C' },
            { id: 3, name: 'C#' },
            { id: 4, name: 'Swift' },
            { id: 5, name: 'Haskell' }
        ];

        var ds = new ListView.DataSource({ rowHasChanged: (a, b) => a !== b })
        return {
            languages: LANGUAGES,
            ds: ds.cloneWithRows(LANGUAGES)
        };
    },

    goToLanguage(language) {
        this.props.navigator.push({
            component: Detail,
            passProps: {
                initialLanguage: language,
                changeName: this.changeName
            }
        });
    },

    changeName(id, newName) {
        var clone = _.cloneDeep(this.state.languages);
        var index = _.findIndex(clone, l => l.id === id);
        clone[index].name = newName;

        RCTDeviceEventEmitter.emit('languageNameChanged', clone[index]);

        this.setState({
            languages: clone,
            ds: this.state.ds.cloneWithRows(clone)
        });
    },

    renderRow(language) {
        return (
            <TouchableHighlight onPress={this.goToLanguage.bind(this, language)}>
                <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', paddingTop: 5, paddingBottom: 5, backgroundColor: '#fff', marginBottom: 1 }}>
                    <Text style={{ marginLeft: 5, marginRight: 5 }}>{language.name}</Text>
                </View>
            </TouchableHighlight>
        );
    },

    render() {
        return (
            <View style={{ flex: 1, backgroundColor: '#ddd' }}>
                <Text style={{ marginTop: 60, marginLeft: 5, marginRight: 5, marginBottom: 10 }}>Select a language</Text>
                <ListView
                    dataSource={this.state.ds}
                    renderRow={this.renderRow} />
            </View>
        );
    }
});

module.exports = List;

detail.js

var React = require('react-native');
var {
    View,
    Text,
    TouchableHighlight
} = React;

var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

var Detail = React.createClass({
    getInitialState() {
        return {
            language: this.props.initialLanguage,
            subscribers: []
        };
    },

    componentDidMount() {
        var subscriber = RCTDeviceEventEmitter.addListener('languageNameChanged', language => {
            this.setState({ language });
        });

        this.setState({
            subscribers: this.state.subscribers.concat([subscriber])
        });
    },

    componentWillUnmount() {
        this.state.subscribers.forEach(sub => {
            console.log('removing');
            sub.remove();
        });
    },

    changeName() {
        this.props.changeName(this.state.language.id, 'Language #' + Math.round(Math.random() * 1000).toString());
    },

    goBack() {
        this.props.navigator.pop();
    },

    render() {
        return (
            <View style={{ flex: 1, backgroundColor: '#ddd', alignItems: 'center', justifyContent: 'center' }}>
                <Text>{this.state.language.name}</Text>

                <TouchableHighlight onPress={this.changeName}>
                    <Text>Click to change name</Text>
                </TouchableHighlight>

                <TouchableHighlight onPress={this.goBack}>
                    <Text>Click to go back</Text>
                </TouchableHighlight>
            </View>
        );
    }
});

module.exports = Detail;
0
On

I wanted to propagate prop change to the rest of route stack as well. And I don't find any way to render renderScene() from the first route.. So I use navigator.replace() instead of updating props. I'm looking for the better way to deal with this because I believe there are a lot of use-case to deal with the route[0] that has info and need to propagate the change to the rest of route stack, like what we do on React props between parent and its children.

# this is on parent component and the change is pushed to props(I'm using Redux)
componentWillReceiveProps(nextProps){
  this.props.hubs.map((currentHub) => {
    nextProps.hubs.map((hub) => {
      if(currentHub.updatedAt !== hub.updatedAt){
        this.props.navigator.getCurrentRoutes().map((r, index) => {
          if(r.getHubId && ( r.getHubId() === hub.objectId ) ){
            let route = Router.getHubRoute(hub);
            this.props.navigator.replaceAtIndex(route, index);
          }
        });
      }
    })
  })