Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cyclic dependency returns empty object in React Native

Tags:

react-native

I have two React Native Components (Alpha and Beta) that navigate to one another; however, this produces a cyclic dependency and React Native doesn’t seem to handle those.

Requiring Beta in Alpha works fine, but requiring Alpha in Beta returns an empty object. An error is thrown when trying to push a route with an invalid component.

Can cyclic dependencies work in React Native? If not, how do I get around this?

Code

index.ios.js

'use strict';

var React = require('react-native');

var Alpha = require('./Alpha');

var {
    AppRegistry,
    NavigatorIOS,
    StyleSheet,
    Text,
    View,
} = React;

var ExampleProject = React.createClass({
    render() {
        return (
            <NavigatorIOS
                style={styles.container}
                initialRoute={{
                    component: Alpha,
                    title: Alpha.title,
                    wrapperStyle: styles.wrapper
                }} />
        );
    },
});

var styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'white'
    },
    wrapper: {
        paddingTop: 64
    }
});

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

Alpha.js

'use strict';

var React = require('react-native');
var Beta = require('./Beta');

var {
    StyleSheet,
    TouchableHighlight,
    View,
    Text
} = React;

var Alpha = React.createClass({
    statics: {
        title: 'Alpha'
    },

    goToBeta() {
        this.props.navigator.push({
            component: Beta,
            title: Beta.title,
            wrapperStyle: styles.wrapper
        });
    },

    render() {
        return (
            <View>
                <TouchableHighlight style={styles.linkWrap}
                    onPress={this.goToBeta}>
                    <Text>Go to Beta</Text>
                </TouchableHighlight>
            </View>
        );
    }
});

var styles = StyleSheet.create({
    linkWrap: {
        flex: 1,
        alignItems: 'center',
        padding: 30
    },
    wrapper: {
        paddingTop: 64
    }
});

module.exports = Alpha;

Beta.js

'use strict';

var React = require('react-native');
var Alpha = require('./Alpha');

var {
    StyleSheet,
    TouchableHighlight,
    View,
    Text
} = React;

var Beta = React.createClass({
    statics: {
        title: 'Beta'
    },

    goToAlpha() {
        this.props.navigator.push({
            component: Alpha,
            title: Alpha.title,
            wrapperStyle: styles.wrapper
        });
    },

    render() {
        return (
            <View>
                <TouchableHighlight style={styles.linkWrap}
                    onPress={this.goToAlpha}>
                    <Text>Go to Alpha</Text>
                </TouchableHighlight>
            </View>
        );
    }
});

var styles = StyleSheet.create({
    linkWrap: {
        flex: 1,
        alignItems: 'center',
        padding: 30
    },
    wrapper: {
        paddingTop: 64
    }
});

module.exports = Beta;
like image 825
Luke Avatar asked Apr 22 '15 20:04

Luke


1 Answers

This is a common problem with routing components. There are a couple of ways to approach this. In general, you want Alpha to avoid requiring Beta before Alpha has defined its exports and vice versa.

Fortunately, JavaScript's import and export keywords address this issue by lazily importing values using an intermediate object that acts as a level of indirection. A specific example is a lot easier to understand.


With import and export

Use the export keyword to export Alpha and Beta from their respective files, and use the import keyword to import them:

// Alpha.js
import Beta from './Beta';

var Alpha = React.createClass({
    /* ... */
});
export default Alpha;

This works because at runtime, the export keyword creates an intermediate object (equivalent to module.exports in CommonJS) and assigns a property named default to it with the value of Alpha. So, the above export statement is conceptually similar to this:

module.exports.default = Alpha;

The files that import Alpha then get a reference to the intermediate object, not Alpha itself until Alpha is directly used. So this code here actually lazily accesses Alpha:

import Alpha from './Alpha';

var ExampleProject = React.createClass({
    render() {
        return (
            <NavigatorIOS
                style={styles.container}
                initialRoute={{
                    component: Alpha,
                    title: Alpha.title,
                    wrapperStyle: styles.wrapper
                }}
            />
        );
    },
});

The lazy access is implemented by running code conceptually similar to this:

var AlphaExports = require('./Alpha');

var ExampleProject = React.createClass({
    render() {
        return (
            <NavigatorIOS
                style={styles.container}
                initialRoute={{
                    component: AlphaExports.default,
                    title: AlphaExports.default.title,
                    wrapperStyle: styles.wrapper
                }}
            />
        );
    },
});

With require()

Using CommonJS's require, you have some other options:

Approach 1: Lazy Loading

Alpha and Beta don't need each other until the user navigates from one scene to the next. For the app to reach this state, Alpha must have defined its exports (that is, module.exports = Alpha must have run for your app to have rendered an Alpha component). So, when the user is navigating to the scene displaying Beta, it is safe for Beta to require Alpha and therefore it is safe to require Beta at this point in time.

// Alpha.js
var Alpha = React.createClass({
    goToBeta() {
        // Lazily require Beta, waiting until Alpha has been initialized
        var Beta = require('./Beta');

        this.props.navigator.push({
            component: Beta,
            title: Beta.title,
            wrapperStyle: styles.wrapper
        });
    }
});

Although it isn't necessary to do the same for Beta.js in this specific scenario because Alpha is the first component loaded, it's probably a good idea so that your components all handle dependency cycles the same way.

Approach 2: Single Module

Another solution is to put Alpha and Beta in the same JS file to remove a cycle between modules. You would then export both components from the new mega-module.

// AlphaBeta.js
var Alpha = React.createClass({...});
var Beta = React.createClass({...});
exports.Alpha = Alpha;
exports.Beta = Beta;

To require it:

// index.js
var {Alpha, Beta} = require('./AlphaBeta');
like image 139
ide Avatar answered Nov 12 '22 19:11

ide