Search code examples
javascriptreactjsreact-nativereusability

React Native TypeError: TypeError: undefined is not an object (evaluating 'this.props.data.map')


I decide to reuse a component that I thought would work for my new application that is pulling in a third-party API.

The reusable component in question is iterating this.props.data.map() which is evaluating as being undefined in my components/Swipe.js file:

import React, { Component } from "react";
import {
  View,
  Animated,
  PanResponder,
  Dimensions,
  LayoutAnimation,
  UIManager
} from "react-native";

const SCREEN_WIDTH = Dimensions.get("window").width;
const SWIPE_THRESHOLD = 0.25 * SCREEN_WIDTH;
const SWIPE_OUT_DURATION = 250;

class Swipe extends Component {
  static defaultProps = {
    onSwipeRight: () => {},
    onSwipeLeft: () => {}
  };

  constructor(props) {
    super(props);

    const position = new Animated.ValueXY();
    const panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (event, gestureState) => true,
      onPanResponderMove: (event, gestureState) => {
        position.setValue({ x: gestureState.dx, y: gestureState.dy });
      },
      onPanResponderRelease: (event, gestureState) => {
        if (gestureState.dx > SWIPE_THRESHOLD) {
          this.forceSwipe("right");
        } else if (gestureState.dx < -SWIPE_THRESHOLD) {
          this.forceSwipe("left");
        } else {
          this.resetPosition();
        }
      }
    });

    this.state = { panResponder, position, index: 0 };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.data !== this.props.data) {
      this.setState({ index: 0 });
    }
  }

  componentWillUpdate() {
    UIManager.setLayoutAnimationEnabledExperimental &&
      UIManager.setLayoutAnimationEnabledExperimental(true);
    LayoutAnimation.spring();
  }

  forceSwipe(direction) {
    const x = direction === "right" ? SCREEN_WIDTH : -SCREEN_WIDTH;
    Animated.timing(this.state.position, {
      toValue: { x, y: 0 },
      duration: SWIPE_OUT_DURATION
    }).start(() => this.onSwipeComplete(direction));
  }

  onSwipeComplete(direction) {
    const { onSwipeLeft, onSwipeRight, data } = this.props;
    const item = data[this.state.index];
    direction === "right" ? onSwipeRight(item) : onSwipeLeft(item);
    this.state.position.setValue({ x: 0, y: 0 });
    this.setState({ index: this.state.index + 1 });
  }

  resetPosition() {
    Animated.spring(this.state.position, {
      toValue: { x: 0, y: 0 }
    }).start();
  }

  getCardStyle() {
    const { position } = this.state;
    const rotate = position.x.interpolate({
      inputRange: [-SCREEN_WIDTH * 1.5, 0, SCREEN_WIDTH * 1.5],
      outputRange: ["-120deg", "0deg", "120deg"]
    });
    return {
      ...position.getLayout(),
      transform: [{ rotate }]
    };
  }

  renderCards() {
    console.log(this.props);
    if (this.state.index >= this.props.data.length) {
      return this.props.renderNoMoreCards();
    }
    return this.props.data
      .map((item, i) => {
        if (i < this.state.index) {
          return null;
        }
        if (i === this.state.index) {
          return (
            <Animated.View
              key={item[this.props.id]}
              style={[this.getCardStyle(), styles.cardStyle]}
              {...this.state.panResponder.panHandlers}
            >
              {this.props.renderCard(item)}
            </Animated.View>
          );
        }
        return (
          <Animated.View
            key={item[this.props.id]}
            style={[styles.cardStyle, { top: 10 * (i - this.state.index) }]}
          >
            {this.props.renderCard(item)}
          </Animated.View>
        );
      })
      .reverse();
  }

  render() {
    return <View>{this.renderCards()}</View>;
  }
}

const styles = {
  cardStyle: {
    position: "absolute",
    width: SCREEN_WIDTH
  }
};

export default Swipe;

I am unclear why this is happening since I do get back a payload: data in my action creator:

export const fetchJobs = (region, callback) => async dispatch => {
  try {
    const url =
      JOB_ROOT_URL +
      JOB_QUERY_PARAMS.key +
      "&method=" +
      JOB_QUERY_PARAMS.method +
      "&category=" +
      JOB_QUERY_PARAMS.keyword +
      "&format=" +
      JOB_QUERY_PARAMS.format;
    let { data } = await axios.get(url);
    dispatch({ type: FETCH_JOBS, payload: data });
    callback();
  } catch (e) {
    console.log(e);
  }
};

So why is data evaluating as undefined in my reusable component?

It's being called here in DeckScreen.js:

import React, { Component } from "react";
import { View, Text } from "react-native";
import { connect } from "react-redux";
import { MapView } from "expo";
import { Card, Button } from "react-native-elements";
import Swipe from "../components/Swipe";

class DeckScreen extends Component {
  renderCard(job) {
    return (
      <Card title={job.title}>
        <View style={styles.detailWrapper}>
          <Text>{job.company}</Text>
          <Text>{job.post_date}</Text>
        </View>
        <Text>
          {job.description.replace(/<span>/g, "").replace(/<\/span>/g, "")}
        </Text>
      </Card>
    );
  }

  render() {
    return (
      <View>
        <Swipe data={this.props.jobs} renderCard={this.renderCard} />
      </View>
    );
  }
}

const styles = {
  detailWrapper: {
    flexDirection: "row",
    justifyContent: "space-around",
    marginBottom: 10
  }
};

function mapStateToProps({ jobs }) {
  return { jobs: jobs.listing };
}

export default connect(mapStateToProps)(DeckScreen);

The button I am pressing that gives me this error is in the MapScreen screen:

import React, { Component } from "react";
import { View, Text, ActivityIndicator } from "react-native";
import { Button } from "react-native-elements";
import { MapView } from "expo";
import { connect } from "react-redux";

import * as actions from "../actions";

class MapScreen extends Component {
  state = {
    region: {
      longitude: 30.2672,
      latitude: 97.7431,
      longitudeDelta: 0.04,
      latitudeDelta: 0.09
    }
  };

  onButtonPress = () => {
    this.props.fetchJobs(this.state.region, () => {
      this.props.navigation.navigate("deck");
    });
  };

  getLocationHandler = () => {
    navigator.geolocation.getCurrentPosition(pos => {
      const currentCoords = {
        longitude: pos.coords.longitude,
        latitude: pos.coords.latitude
      };

      this.goToLocation(currentCoords);
    });
  };

  goToLocation = coords => {
    this.map.animateToRegion({
      ...this.state.region,
      longitude: coords.longitude,
      latitude: coords.latitude
    });
    this.setState(prevState => {
      return {
        region: {
          ...prevState.region,
          longitude: coords.longitude,
          latitude: coords.latitude
        }
      };
    });
  };

  render() {
    return (
      <View style={{ flex: 1 }}>
        <MapView
          initialRegion={this.state.region}
          style={{ flex: 1 }}
          ref={ref => (this.map = ref)}
        />
        <View style={styles.buttonContainer}>
          <Button
            title="Search This Area"
            icon={{ name: "search" }}
            onPress={this.onButtonPress}
          />
        </View>
        <View>
          <Button
            title="My Location"
            icon={{ name: "map" }}
            onPress={this.getLocationHandler}
          />
        </View>
      </View>
    );
  }
}

const styles = {
  buttonContainer: {
    position: "absolute",
    bottom: 50,
    left: 0,
    right: 0
  }
};

export default connect(
  null,
  actions
)(MapScreen);

This should be an array of objects as verified here: enter image description here

And in my reducer I have:

import { FETCH_JOBS } from "../actions/types";

const INITIAL_STATE = {
  listing: []
};

export default function(state = INITIAL_STATE, action) {
  switch (action.type) {
    case FETCH_JOBS:
      return action.payload;
    default:
      return state;
  }
}

I added some verbose error handling and this is what I got back:

[02:25:28] fetchJobs Action Error: Given action "fetch_jobs", reducer "jobs" returned undefined. To ignore an action, you must explicitly return the previous state. If you want this reducer to hold no value, you can return null instead of undefined.

So it seems like the problem is in the jobs_reducer:

import { FETCH_JOBS } from "../actions/types";

const INITIAL_STATE = {
  listing: []
};

export default function(state = INITIAL_STATE, action) {
  switch (action.type) {
    case FETCH_JOBS:
      return action.payload;
    default:
      return state;
  }
}

I don't know if I am just too exhausted at this point, but I have tried listings: [], I have tried listing: [], I am out of ideas of how to get this reducer to not return undefined because even when I do this:

import { FETCH_JOBS } from "../actions/types";

// const INITIAL_STATE = {
//   listing: []
// };

export default function(state = null, action) {
  switch (action.type) {
    case FETCH_JOBS:
      return action.payload;
    default:
      return state;
  }
}

I get the same error message.

My idea with creating an INITIAL_STATE and setting it to listing: [] is to ensure I could map over this array and never worry about the case where I have not yet fetched the list of jobs.

So I am perplexed as to exactly where I am getting this undefined since I did set the initial state to null and I was still getting that error.

So in the process of debugging I then tried this:

import { FETCH_JOBS } from "../actions/types";

// const INITIAL_STATE = {
//   listing: []
// };

export default function(state = null, action) {
  console.log("action is", action);
  switch (action.type) {
    case FETCH_JOBS:
      return action.payload;
    default:
      return state;
  }
}

And got that the payload is undefined:

Please check your inputs.
[09:39:38] action is Object {
[09:39:38]   "payload": undefined,
[09:39:38]   "type": "fetch_jobs",
[09:39:38] }

I have hit a wall here. I did a whole refactor to my jobs action creator and logged out the payload property:

export const fetchJobs = (region, distance = 10) => async dispatch => {
  try {
    const url = buildJobsUrl();
    let job_list = await axios.get(url);
    job_list = locationify(
      region,
      console.log(job_list.data.listings.listing),
      job_list.data.listings.listing,
      distance,
      (obj, coords) => {
        obj.company.location = { ...obj.company.location, coords };
        return obj;
      }
    );
    dispatch({ type: FETCH_JOBS, payload: job_list });
  } catch (e) {
    console.log("fetchJobs Action Error:", e.message);
  }
};

The console.log(job_list.data.listings.listing) logged out the data to my terminal successfully and yet my payload property is still undefined, how is that possible?

I got the action creator and reducer working by refactoring the action creator to just this:

import axios from "axios";
import { Location } from "expo";
import qs from "qs";

import { FETCH_JOBS } from "./types";
// import locationify from "../tools/locationify";

const JOB_ROOT_URL = "https://authenticjobs.com/api/?";

const JOB_QUERY_PARAMS = {
  api_key: "<api_key>",
  method: "aj.jobs.search",
  perpage: "10",
  format: "json",
  keywords: "javascript"
};

const buildJobsUrl = zip => {
  const query = qs.stringify({ ...JOB_QUERY_PARAMS });
  return `${JOB_ROOT_URL}${query}`;
};

export const fetchJobs = (region, callback) => async dispatch => {
  try {
    let zip = await Location.reverseGeocodeAsync(region);
    const url = buildJobsUrl(zip);
    console.log(url);
    let { data } = await axios.get(url);
    dispatch({ type: FETCH_JOBS, payload: data });
    callback();
  } catch (e) {
    console.error(e);
  }
};

So the problem is no longer there in theory, right. Then, when I bring in the Swipe.js component, the problem returns, in particular the problem seems to be with this code here:

renderCards() {
    if (this.state.index >= this.props.data.length) {
      return this.props.renderNoMoreCards();
    }

    return this.props.data
      .map((item, i) => {
        if (i < this.state.index) {
          return null;
        }

        if (i === this.state.index) {
          return (
            <Animated.View
              key={item[this.props.id]}
              style={[this.getCardStyle(), styles.cardStyle]}
              {...this.state.panResponder.panHandlers}
            >
              {this.props.renderCard(item)}
            </Animated.View>
          );
        }
        return (
          <Animated.View
            key={item[this.props.id]}
            style={[styles.cardStyle, { top: 10 * (i - this.state.index) }]}
          >
            {this.props.renderCard(item)}
          </Animated.View>
        );
      })
      .reverse();
  }

This is where I start to hit a roadblock again.


Solution

  • Looks like what helped was refactoring my jobs_reducer file from:

    import { FETCH_JOBS } from "../actions/types";
    
    const INITIAL_STATE = {
      listing: []
    };
    
    export default function(state = INITIAL_STATE, action) {
      switch (action.type) {
        case FETCH_JOBS:
          return action.payload;
        default:
          return state;
      }
    }
    

    to this:

    export default function(state = INITIAL_STATE, action) {
      switch (action.type) {
        case FETCH_JOBS:
          const { listings } = action.payload;
          return { ...state, listing: listings.listing };
        default:
          return state;
      }
    }