Search code examples
javascriptreactjsflux

cannot read property of undefined when initialize state in react


I'm trying to make a forecast app with React and Flux. I fetch the data from Yahoo Weather API, and put the data to my store with a callback in jsonp request.Then in the View, I get the data (in componentDidMount())from store as a state and pass some properties of it to child components.

The data(this.state.store), which is a Object, has two properties, called condition and forecast.The problem is that if I want to pass the this.state.store.condition(or forecast) to the child, it says TypeError: Cannot read property 'condition' of undefined. But if I just try to access this.state.store(for example, console.log(this.state.store)), there is no error.

Also, if I try to access this.state.store.condition in a try-catch statement, and log the error when there is one, I do access the condition successfully with the console printed TypeError above mentioned.

Here is my codes:

store:

const CHANGE_EVENT = 'change';
let _app = {};
//  create a city
function create(city, data) {
    _app[city.toUpperCase()] = {
        condition: data.condition,
        forecast: data.forecast,
    };
}
const AppStore = Object.assign({}, EventEmitter.prototype, {
    getAll() {
        return _app;
    },
    emitChange() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
});
//  register callback
AppDispatcher.register((action) => {
    switch (action.actionType) {
        case AppConstants.CREATE_CITY: {
            create(action.city, action.data);
            AppStore.emitChange();
            break;
        }
        // other cases

        default:
            //  noop
    }
});

actions:

function callback(city, data) {
    console.log(data);
    const action = {
        actionType: AppConstants.CREATE_CITY,
        city,
        data,
    };
    AppDispatcher.dispatch(action);
}

const AppActions = {
    create(city) {
        getDataFromAPI(city, callback);
    },
};

utils:

function getDataFromAPI(query, callback) {
    let data;
    const url = `https://query.yahooapis.com/v1/public/yql?q=select * from weather.forecast where u='c' AND  woeid in (select woeid from geo.places(1) where text="${query}")&format=json`;
    superagent
        .get(url)
        .use(jsonp)
        .end((err, res) => {
            console.log(res.body.query.results.channel.item);
            data = res.body.query.results.channel.item;
            callback(query, data);
        });
}

views:

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            store: Store.getAll(),
            currentCity: 'BEIJING',
        };
        this.onChange = this.onChange.bind(this);
        this.getCurrentCity = this.getCurrentCity.bind(this);
    }
    componentWillMount() {
        AppActions.create('BEIJING');
    }
    componentDidMount() {
        Store.addChangeListener(this.onChange);
    }
    onChange() {
        this.setState({ store: Store.getAll() });
    }
    getCurrentCity(city) {
        this.setState({ currentCity: city.toUpperCase() });
    }
    componentWillUnmout() {
        Store.removeChangeListener(this.onChange);
    }
    render() {
        // For now, I have to do all of these to pass the condition to the child component
        let condition;
        let forecast;
        let text;
        let temp;
        let currentWeatherCode;
        let forecastWeatherCode = [];
        let currentWeatherClassName;
        let forecastWeatherClassName = [];
        let date;
        let forecastDate = [];
        console.log(this.state.store[this.state.currentCity]);<--NO ERROR 

        // console.log(this.state.store[this.state.currentCity])<--UNDEFINED
        // console.log(this.state.store[this.state.currentCity].condition);<--CANNOT READ PROPERTY
                 ^
                 |
               ERROR ON THIS 2 STATEMENTS           

        try {
            condition = this.state.store[this.state.currentCity].condition;
            forecast = this.state.store[this.state.currentCity].forecast;
            text = condition.text.toUpperCase();
            temp = condition.temp;
            currentWeatherCode = condition.code;
            currentWeatherClassName = setWeatherIcon(currentWeatherCode);
            date = condition.date;
            for (let i = 0; i < 6; i++) {
                forecastWeatherCode.push(forecast[i].code);
                forecastWeatherClassName.push(setWeatherIcon(forecastWeatherCode[i]));
                forecastDate.push(forecast[i].date);
            }
        } catch (err) {
            console.log(err);<--STILL ERROR, BUT I DO ACCESS THE PROP CONDITION IN THIS WAY
        }
        return (
            <div>
                <Today
                    city={this.state.currentCity}
                    weatherStatus={text}
                    tempreture={temp}
                    currentWeatherClassName={currentWeatherClassName}
                    date={date}
                />
            </div>
        );
    }
}

ReactDOM.render(<App />, document.querySelector('#app'));

Solution

  • It seems to me that you are trying to access the this.state.store[this.state.currentCity] property before it is fetched from the remote API.

    You could add some sort of indication that the data is still being fetched like this.

    render() {
            // For now, I have to do all of these to pass the condition to the child component
            let condition;
            let forecast;
            let text;
            let temp;
            let currentWeatherCode;
            let forecastWeatherCode = [];
            let currentWeatherClassName;
            let forecastWeatherClassName = [];
            let date;
            let forecastDate = [];
            console.log(this.state.store[this.state.currentCity]);<--NO ERROR
    
            if (!this.state.store.hasOwnProperty(this.state.currentCity)) {
                    return <div>Loading...</div>;
            }
    
            ... the rest of your original code
    }
    

    When it is done loading the setState() method is invoked and render() is called again. The second time it will fall trough the if and run your code.