dan-klasson
dan-klasson

Reputation: 14210

Last child component overwrites all children

I have a parent react component containing 3 children:

<ChildComponent category="foo" />
<ChildComponent category="bar" />
<ChildComponent category="baz" />

The child component calls an api depending on the prop category value:

http://example.com/listings.json?category=foo

In my action the data is returned as expected. However, when the child component renders the data. The last child baz is overwriting its value in foo and bar as well.

A solution to this problem seems to be given here. But I would like this to be dynamic and only depend on the category prop. Is this not possible to do in Redux?

My child component looks like this:

class TweetColumn extends Component {

  componentDidMount() {
    this.props.fetchTweets(this.props.column)
  }

  render() {
    const { tweets, column } = this.props
    if (tweets.length === 0) { return null }
    const tweetItems = tweets[column].map(tweet => (
      <div key={ tweet.id }>
        { tweetItems.name }
      </div>
    )
    return (
      <div className="box-content">
        { tweetItems }
      </div>
    )
  }
}

TweetColumn.propTypes = {
  fetchTweets: PropTypes.func.isRequired,
  tweets: PropTypes.array.isRequired
}

const mapStateToProps = state => ({
  tweets: state.tweets.items
})

export default connect(mapStateToProps, { fetchTweets })( TweetColumn )

reducers:

export default function tweetReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_TWEETS_SUCCESS:
      return {
    ...state,
    [action.data[0].user.screen_name]: action.data
      }
    default:
      return state;
  }
}

export default combineReducers({
    tweets: tweetReducer,
})

action:

export const fetchTweets = (column) => dispatch => {
  dispatch({ type: FETCH_TWEETS_START })
  const url = `${ TWITTER_API }/statuses/user_timeline.json?count=30&screen_name=${ column }`
  return axios.get(url)
    .then(response => dispatch({
      type: FETCH_TWEETS_SUCCESS,
      data: response.data
    }))
    .then(response => console.log(response.data))
    .catch(e => dispatch({type: FETCH_TWEETS_FAIL}))

}

Upvotes: 0

Views: 288

Answers (1)

nebuler
nebuler

Reputation: 1545

You are making an api call every time TweetColumn is mounted. If you have multiple TweetColumn components and each one makes an api call, then whichever one's response is last to arrive is going to set the value of state.tweets.items. That's because you are dispatching the same action FETCH_TWEETS_SUCCESS every time (the last one overrides the previous one). To solve that issue, assuming the response has a category (foo, bar, baz), I would write the reducer in the following way:

export default function tweetReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_TWEETS_SUCCESS:
      return {
          ...state,
          [action.data.category]: action.data
      }
    default:
      return state;
  }
}

You can then do the following in your TweetColumn component:

class TweetColumn extends Component {

  componentDidMount() {
    this.props.fetchTweets(this.props.column)
  }

  render() {
    const { column } = this.props;
    const tweetItems = this.props.tweets[column].map(tweet => (
      <div key={ tweet.id }>
        { tweet.name }
      </div>
    )
    return (
      <div className="box-content">
        { tweetItems }
      </div>
    )
  }
}

const mapStateToProps = state => ({
  tweets: state.tweets
})

const mapDispatchToProps = dispatch => ({
  fetchTweets: column => dispatch(fetchTweets(column))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)( TweetColumn )

You will have to do some validation to make sure tweets[column] exists, but you get the idea.

Upvotes: 2

Related Questions