crash springfield
crash springfield

Reputation: 1082

Updating component state in React-Redux with API calls

I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well. I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.

There are two related problems:

FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.

SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.

Here's my component below:

class Hike extends Component {
  constructor() {
    super()
    this.state = {
      currentHike: {
        id: '',
        name: '',
        review: {},
      }
    }
  }

  componentDidUpdate() {
    const params = this.props.params
    const hack = "/api/hike/" + params
    // APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
    APIManager.get(hack, null, (err, response) => { // works
      if (err) {
        console.error(err)
        return
      }
      console.log(JSON.stringify(response.result)) // SECOND

      this.setState({
        currentHike: response.result
      })
    })
  }

  render() {
    // Allow for fields to be blank
    const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name

    return (
      <div>
        <p>testing hike component</p>
        <p>{this.state.currentHike.name}</p>
      </div>
    )
  }
}

const stateToProps = (state) => {
  return {
    params: state.hike.selectedHike
  }
}

export default connect(stateToProps)(Hike)

Also: When I click a link on the page to go to another url, I get the following error:

"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."

Upvotes: 3

Views: 16194

Answers (5)

Derrick
Derrick

Reputation: 1606

Looking at your code, I think I would architect it slightly differently

Few things:

  1. Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk

example:

function fetchHikeById(hikeId) {
    return dispatch => {
        // optional: dispatch an action here to change redux state to loading
        dispatch(action.loadingStarted())
        const hack = "/api/hike/" + hikeId
        APIManager.get(hack, null, (err, response) => {
            if (err) {
                console.error(err);
                // if you want user to know an error happened.
                // you can optionally dispatch action to store
                // the error in the redux state. 
                dispatch(action.fetchError(err));
                return;
             }
             dispatch(action.currentHikeReceived(response.result))
        });
    } 
}

You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.

  1. Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.

The lifecycle method you put the fetchHikeById action is.

componentDidMount() {
    // assume you are using react router to pass the hikeId 
    // from the url '/hike/:hikeId' 
    const hikeId = this.props.params.hikeId;
    this.props.fetchHikeById(hikeId);
}

componentWillReceiveProps(nextProps) {
   // so this is when the props changed.
   // so if the hikeId change, you'd have to re-fetch.  
   if (this.props.params.hikeId !== nextProps.params.hikeId) {
       this.props.fetchHikeById(nextProps.params.hikeId)
   }
}

Upvotes: 5

crash springfield
crash springfield

Reputation: 1082

Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to. Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.

Hike.js

class Hike extends Component {
  constructor() {
    super()
    this.state = {}
  }

  componentDidUpdate() {
     console.log('dealing with ' + JSON.stringify(this.props.currentHike))
  }

  render() {

    if (this.props.currentHike == null || undefined) { return false }
    const currentHike = this.props.currentHike

    return (
      <div className="sidebar">
        <p>{currentHike.name}</p>
      </div>
    )
  }
}

const stateToProps = (state) => {
  return {
    currentHike: state.hike.currentHike,
  }
}

And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.

Map.js

onMarkerClick(id) {
    const hikeId = id
    // Set params to be fetched
    this.props.hikeSelected(hikeId)
    // GET hike data from database
    const hack = "/api/hike/" + hikeId
    APIManager.get(hack, null, (err, response) => {
      if (err) {
        console.error(err)
        return
      }
      this.props.currentHikeReceived(response.result)
    })
    // Change path to clicked hike
    const path = `/hike/${hikeId}`
    browserHistory.push(path)
  }

const stateToProps = (state) => {
  return {
    hikes: state.hike.list,
    location: state.newHike
  }
}

const dispatchToProps = (dispatch) => {
    return {
    currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
    hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
    hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
    locationAdded: (location) => dispatch(actions.locationAdded(location)),
    }
}

Upvotes: 0

aldaty
aldaty

Reputation: 11

The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.

The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:

  1. Some state changes in redux.
  2. Hike receives new props (or its state changes).
  3. Hike renders according to the new props.
  4. Hike has now been updated and calls your "componentDidUpdate" function.
  5. componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
  6. Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.

When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.

The docs explain this really well I find, I would give those a thorough read.

Upvotes: 1

Josh Beam
Josh Beam

Reputation: 19772

Try making the API call in componentDidMount:

componentDidMount() {
  // make your API call and then call .setState
}

Do that instead of inside of componentDidUpdate.

There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:

Root Component

This is the simplest approach so it's great for prototypes and small apps.

With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.

As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.

By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.

Upvotes: 0

CharlieBrown
CharlieBrown

Reputation: 4163

I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)

Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D

You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.

https://facebook.github.io/react/docs/react-component.html#componentdidupdate

However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.

Upvotes: 4

Related Questions