Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React JS: Component not updating, after calling setState

I've got a small react app I'm playing with, just go get the hang of the library. The app is just a series of lists, which are populated from a server. When a list item is clicked, the value of that item is added to a list filters at the app level, which will then be used to call new data to populate the lists.

The problem is that I can't seem to get my lists to reconcile with the new data from the app (parent), even when calling setState. Here's my code (coffee):

### 
@jsx React.DOM
###

{div, h1, h2, h4, ul, li, form, input, br, p, strong, span, a} = React.DOM

SearchApp = React.createClass
  handleTopItemClick: (filter) ->
    facet = filter.field
    filters = @state.filters
    if filters.facets[facet] and filters.facets[facet].length > 0
      filters.facets[facet].push filter.value
    else 
      filters.facets[facet] = [filter.value]

    strArr = []
    _.each filters.facets, (valArr, field) ->
      _.each valArr, (val) ->
        strArr.push "+(#{field}:\"#{val}\")" 

    @setState 
      filters: filters
      queryStr: strArr.join(' ').trim()

  getInitialState: ->
    filters:
      facets: {}
    queryStr: ''    

  render: ->
    (div {
      id: 'content'
      className: "search-wrap"
      },
      (h1 {}, "Search")
      (div
        id: 'widgets',
        (TopList
          title: 'Top Domains'
          params:
            query: @state.queryStr
            field: 'domain'
          onItemClick: @handleTopItemClick
        )
        (TopList
          title: 'Top Senders'
          params:
            query: @state.queryStr
            field: 'from'
          onItemClick: @handleTopItemClick
        )
        (TopList
          title: 'Top Recipient'
          params:
            query: @state.queryStr
            field: 'recipient'
          onItemClick: @handleTopItemClick
        ) 
      )        
    )

TopItem = React.createClass
  getDefaultProps: ->
    value: ''
    count: 0
    field: null

  render: ->
    (li {},
      (a {
        onClick: @handleClick
        className: 'top-item-filter'
        title: @props.value
        },
        (strong {}, @props.value)
        (span {}, @props.count)
      )
    )

  handleClick: (event) ->
    event.preventDefault()
    @props.onItemClick @props.value

TopList = React.createClass
  getInitialState: ->
    data: []
    params: @props.params

  componentWillReceiveProps: (nextProps) ->
    @setState params: nextProps.params

  componentWillMount: ->
    request.post("/facet").send(@state.params).end (results) =>
      @setState data: JSON.parse(results.text)

  render: ->
    itemNodes = _.map @state.data, (item) =>
      key = item.value
      TopItem
        value: item.value
        count: item.count
        key: key
        onItemClick: @handleItemClick

    (div {className: 'widget top-item'},
      (h2 {className: 'widget-header'}, "#{@props.title}")
      (ul {className: 'top-items-list'}, itemNodes)
    )

  handleItemClick: (value) ->
    @props.onItemClick
      value: value
      field: @props.params.field

React.renderComponent SearchApp(null), document.getElementById("content")

The lists all render fine the first time around, fetching the unfiltered data as expected. When I click on a list item, the SearchApp receives the event, and updates its own state accordingly. componentWillReceiveProps is properly called on the TopList classes, but the setState call there doesn't update their state, and thus, they aren't reconciling. I've verified that nextProps contains the new information. What am I missing?

like image 734
Kevin Whitaker Avatar asked Mar 14 '14 13:03

Kevin Whitaker


1 Answers

There isn't really any reason to save the props in state; and it's much less error prone to keep props as the source of truth. It also simplifies the code a bit.

For the actual issue, though; componentWillMount is only called once here. If you want to repeat the AJAX request when new params are passed; you can do that like so:

TopList = React.createClass
  getInitialState: ->
    data: []

  getSearchResultsFromServer: (params) ->
    request.post("/facet").send(params).end (results) =>
      if @isMounted()
        @setState data: JSON.parse(results.text) 

  componentWillReceiveProps: (nextProps) -> @getSearchResultsFromServer nextProps.params
  componentDidMount: -> @getSearchResultsFromServer @props.params

  render: ->
    itemNodes = _.map @state.data, (item) =>
      key = item.value
      TopItem
        value: item.value
        count: item.count
        key: key
        onItemClick: @handleItemClick

    (div {className: 'widget top-item'},
      (h2 {className: 'widget-header'}, "#{@props.title}")
      (ul {className: 'top-items-list'}, itemNodes)
    )

  handleItemClick: (value) ->
    @props.onItemClick
      value: value
      field: @props.params.field

Ideally, your ajax library would support aborting requests; and in componentWillUnmount you abort those requests.

like image 126
Brigand Avatar answered Oct 20 '22 01:10

Brigand