3

I'm trying to move forward with ES6 syntax but I've got an error trying to retrieve state value.

So my question is : how to get the state value in ES6? Here is a part of the code:

constructor(props) {
    super(props);
    this.state = {
        timeElapsed: null,
        isRunning: false
    }
}

Then when I try to get isRunning state, it gives me this error: Cannot read property 'isRunning' of undefined.

if (this.state.isRunning) {

    clearInterval(this.interval);
    this.setState({
        isRunning: false
    });
    return

}

Any idea? Thanks.

EDIT (here is the full code):

import React, {Component} from 'react';

import {
    Text,
    View,
    AppRegistry,
    StyleSheet,
    TouchableHighlight
} from 'react-native';

import Moment from 'moment';

import formatTime from 'minutes-seconds-milliseconds';

class StopWatch extends Component {

    constructor(props) {
        super(props);
        this.state = {
            timeElapsed: null,
            isRunning: false
        }
    }

    render() {
        return (
            <View style={styles.container}>
                <View style={styles.header}>
                    <View style={styles.timerWrapper}>
                        <Text style={styles.timer}>{formatTime(this.state.timeElapsed)}</Text>
                    </View>
                    <View style={styles.buttonWrapper}>
                       {this.startStopButton()}
                       {this.lapButton()}
                    </View>
                </View>
                <View style={styles.footer}>
                    <Text>List of laps</Text>
                </View>
            </View>
        )
    }

    startStopButton() {
        var style = this.state.isRunning ? styles.startButton : styles.stopButton;
        return (
            <TouchableHighlight style={[styles.button, style]} onPress={this.handleStartPress} underlayColor="gray">
                <Text>{this.state.isRunning ? 'Stop' : 'Start'}</Text>
            </TouchableHighlight>
        )
    }

    lapButton() {
        return (
            <TouchableHighlight style={[styles.button, styles.lapButton]} onPress={this.lapPress} underlayColor="gray">
                <Text>Lap</Text>
            </TouchableHighlight>
        )
    }

    border(color) {
        return {
            borderColor: color,
            borderWidth: 4
        }
    }

    handleStartPress() {
        console.log('Start was pressed');

        if (this.state.isRunning) {

            clearInterval(this.interval);
            this.setState({
                isRunning: false
            });
            return

        }

        var startTime = new Date();

        this.interval = setInterval(
            ()=>{
                this.setState({
                    timeElapsed: new Date() - startTime
                })
            }, 30
        );

        this.setState({
            isRunning: true
        })

    }

    lapPress() {
        console.log('Lap was pressed');
    }

}

var styles = StyleSheet.create({
    container: { // Main container
        flex: 1,
        alignItems: 'stretch'
    },
    header: { // Yellow
        flex: 2
    },
    footer: { // Blue
        flex: 3
    },
    timerWrapper: {
        flex: 5,
        justifyContent: 'center',
        alignItems: 'center'
    },
    timer: {
        fontSize: 60
    },
    buttonWrapper: {
        flex: 3,
        flexDirection: 'row',
        justifyContent: 'space-around',
        alignItems: 'center'
    },
    button: {
        borderWidth: 2,
        height: 100,
        width: 100,
        borderRadius: 50,
        justifyContent: 'center',
        alignItems: 'center'
    },
    startButton: {
        borderColor: 'red'
    },
    stopButton: {
        borderColor: 'green'
    },
    lapButton: {
        borderColor: 'blue'
    }
});

// AppRegistry.registerComponent('stopWatch', function() {
//     return StopWatch;
// });

AppRegistry.registerComponent('stopwatch', () => StopWatch);

EDIT 2:

Here is a combined solution with and without binding in constructor:

import React, {Component} from 'react';

import {
    Text,
    View,
    AppRegistry,
    StyleSheet,
    TouchableHighlight
} from 'react-native';

import Moment from 'moment';

import formatTime from 'minutes-seconds-milliseconds';

class StopWatch extends Component {

    constructor(props) {
        super(props);

        this.state = {
            timeElapsed: null,
            isRunning: false
        }

        this.startStopButton = this.startStopButton.bind(this)
        this.lapButton = this.lapButton.bind(this)
    }

    render() {
        return (
            <View style={styles.container}>
                <View style={styles.header}>
                    <View style={styles.timerWrapper}>
                        <Text style={styles.timer}>{formatTime(this.state.timeElapsed)}</Text>
                    </View>
                    <View style={styles.buttonWrapper}>
                       {this.startStopButton()}
                       {this.lapButton()}
                    </View>
                </View>
                <View style={styles.footer}>
                    <Text>List of laps</Text>
                </View>
            </View>
        )
    }

    startStopButton() {

        var style = this.state.isRunning ? styles.startButton : styles.stopButton;

        handleStartPress = () => {
            console.log('Start was pressed');

            if (this.state.isRunning) {

                clearInterval(this.interval);
                this.setState({
                    isRunning: false
                });
                return

            }

            var startTime = new Date();

            this.interval = setInterval(
                ()=>{
                    this.setState({
                        timeElapsed: new Date() - startTime
                    })
                }, 30
            );

            this.setState({
                isRunning: true
            })
        }

        return (
            <TouchableHighlight style={[styles.button, style]} onPress={handleStartPress} underlayColor="gray">
                <Text>{this.state.isRunning ? 'Stop' : 'Start'}</Text>
            </TouchableHighlight>
        )

    }

    lapButton() {

        handleLapPress = () => {
            console.log('Lap was pressed');
        }

        return (
            <TouchableHighlight style={[styles.button, styles.lapButton]} onPress={handleLapPress} underlayColor="gray">
                <Text>Lap</Text>
            </TouchableHighlight>
        )

    }

    border(color) {

        return {
            borderColor: color,
            borderWidth: 4
        }

    }

}

var styles = StyleSheet.create({
    container: { // Main container
        flex: 1,
        alignItems: 'stretch'
    },
    header: { // Yellow
        flex: 2
    },
    footer: { // Blue
        flex: 3
    },
    timerWrapper: {
        flex: 5,
        justifyContent: 'center',
        alignItems: 'center'
    },
    timer: {
        fontSize: 60
    },
    buttonWrapper: {
        flex: 3,
        flexDirection: 'row',
        justifyContent: 'space-around',
        alignItems: 'center'
    },
    button: {
        borderWidth: 2,
        height: 100,
        width: 100,
        borderRadius: 50,
        justifyContent: 'center',
        alignItems: 'center'
    },
    startButton: {
        borderColor: 'red'
    },
    stopButton: {
        borderColor: 'green'
    },
    lapButton: {
        borderColor: 'blue'
    }
});

AppRegistry.registerComponent('stopwatch', () => StopWatch);
Xavier C.
  • 1,809
  • 4
  • 24
  • 40
  • Your second snippet - is that inside of a classes `render` method? Or is it in another class method? If it's another class method, make sure it is bound to `this` correctly. – Cody Reichert May 06 '16 at 15:13
  • Show us your function, maybe your `this` is gone, do you use arrow function? – zooblin May 06 '16 at 15:14
  • I've added the full code. – Xavier C. May 06 '16 at 15:17
  • you pass `handleStartPress` as callback, you have to `bind` `handleStartPress` function to your current `this`, use arrow function syntax `handleStartPress = () => {//code}` – zooblin May 06 '16 at 15:22
  • @zooblin : What is the best solution? Include handleStartPress method into startStopButton component or let handleStartPress method outside and bind it from constructor? – Xavier C. May 06 '16 at 15:51
  • Possible duplicate of [Unable to access React instance (this) inside event handler](http://stackoverflow.com/questions/29577977/unable-to-access-react-instance-this-inside-event-handler) – Felix Kling May 06 '16 at 15:56
  • Related: [How to access the correct `this` / context inside a callback?](http://stackoverflow.com/q/20279484/218196) – Felix Kling May 06 '16 at 15:57
  • 1
    in my opinion let handleStartPress method outside and bind it from constructor preferable – zooblin May 06 '16 at 16:15
  • Is that useful to bind components also and not only methods? Because I don't have any problem with **this** when I render components by using **{this.startStopButton()}** in *render* when the component is not bound in constructor. I might not understand something... – Xavier C. May 06 '16 at 16:55
  • You should read both questions I linked to... especially the second one. – Felix Kling May 06 '16 at 17:54
  • Yes thank you @FelixKling! – Xavier C. May 07 '16 at 10:50

2 Answers2

3

You need to bind your class methods to the correct this. See the facebook documentation on using ES6 classes: https://facebook.github.io/react/docs/reusable-components.html#es6-classes.

To fix your error, bind your methods inside of your classes constructor:

class StopWatch extends Component {

constructor(props) {
    super(props);

    this.state = {
        timeElapsed: null,
        isRunning: false
    }

    this.startStopButton= this.startStopButton.bind(this)
    this.lapButton = this.lapButton.bind(this)
    this.handleStartPress = this.handleStartPress.bind(this)
    this.handleLap = this.handleLap.bind(this)
}

render() {
    return (
        <View style={styles.container}>
            <View style={styles.header}>
                <View style={styles.timerWrapper}>
                    <Text style={styles.timer}>{formatTime(this.state.timeElapsed)}</Text>
                </View>
                <View style={styles.buttonWrapper}>
                   {this.startStopButton()}
                   {this.lapButton()}
                </View>
            </View>
            <View style={styles.footer}>
                <Text>List of laps</Text>
            </View>
        </View>
    )
}

startStopButton() {
    var style = this.state.isRunning ? styles.startButton : styles.stopButton;
    return (
        <TouchableHighlight style={[styles.button, style]} onPress={this.handleStartPress} underlayColor="gray">
            <Text>{this.state.isRunning ? 'Stop' : 'Start'}</Text>
        </TouchableHighlight>
    )
}

lapButton() {
    return (
        <TouchableHighlight style={[styles.button, styles.lapButton]} onPress={this.lapPress} underlayColor="gray">
            <Text>Lap</Text>
        </TouchableHighlight>
    )
}

border(color) {
    return {
        borderColor: color,
        borderWidth: 4
    }
}

handleStartPress() {
    console.log('Start was pressed');

    if (this.state.isRunning) {

        clearInterval(this.interval);
        this.setState({
            isRunning: false
        });
        return

    }

    var startTime = new Date();

    this.interval = setInterval(
        ()=>{
            this.setState({
                timeElapsed: new Date() - startTime
            })
        }, 30
    );

    this.setState({
        isRunning: true
    })

}

lapPress() {
    console.log('Lap was pressed');
}

}

sorry for the bad formatting

Cody Reichert
  • 1,620
  • 15
  • 13
2

This has nothing to do with ES6, it's an inherent difficulty with the way Javascript works.

Your error is here:

onPress={this.lapPress}

You are thinking that means "when the button is pressed, invoke the method lapPress on my component". It doesn't. It means "when the button is pressed, invoke the method lapPress from my component, using whatever this happens to be set to".

There are several ways to bind this to its method properly, but the easiest (in ES6) might be

onPress={() => this.lapPress()}

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • this is not best approach, on each `render` you create a new `function`, the better way is to pass a callback to arrow function – zooblin May 06 '16 at 15:28
  • 3
    "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time." – Donald Knuth – Michael Lorton May 06 '16 at 17:00