Search code examples
javascriptreactjsreduxredux-thunk

How to disable a button while waiting for redux to resolve?


In following example, how can I disable the button during a geolocation request? In this.props.inProgress isn't set on init, I would like to disable button when getCurrentPosition is requested and enable if RECEIVE_LOCATION is resolved. What is correct approach? Do I to use state and copy props to the GeoButton?

export function getGeolocation() {
  return dispatch => {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(function(position) {
        dispatch({
          type: 'RECEIVE_LOCATION',
          coords: {
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            inProgress: false,
          },
        });
      });
    }
  }
}
export function geolocation(state={}, action) {
  switch (action.type) {
    case 'RECEIVE_LOCATION':
      var newState = action.coords;

      return newState;
    default:
      return state;
  }
}


class GeoButton extends React.Component {
  constructor(props) {
    super(props);
  }

  findLocation(e) {
    e.preventDefault();
    this.props.dispatch(getGeolocation());
  }
  render() {
    console.log(this.props); // on init geolocation object is empty
    var self = this;
    return (
      <div>
        <button type="button" onClick={this.findLocation} disabled={self.props.geolocation.inProgress}>Get location</button>
      </div>
    )
  }
}

export default connect(state => ({
  geolocation: state.geolocation
}))(GeoButton); // just gives it dispatch()

Solution

  • When doing async in redux, you often need to call dispatch twice. One synchronous, one asynchronous.

    Your action should look something like this:

    export function getGeolocation() {
      return dispatch => {
        dispatch({ type: 'FETCHING_LOCATION' });
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition((position) => {
            dispatch({
              type: 'RECEIVE_LOCATION',
              coords: {
                latitude: position.coords.latitude,
                longitude: position.coords.longitude
              }
            });
          });
        }
      };
    }
    

    And your reducer should look like this. I've tweaked the structure of the state object, to separate the app data from the ui data.

    export function geolocation(state = {}, action) {
      switch (action.type) {
        case 'RECEIVE_LOCATION':
          return {
            coords: action.coords,
            inProgress: false
          };
        case 'FETCHING_LOCATION':
          return {
            coords: null,
            inProgress: true
          };
      }
      return state;
    }
    

    There's no need to have the inProgress flag set within your action creator. The reducer can derive it from the action type.