Katie
Katie

Reputation: 907

connected react-redux component does not rerender upon state

I am trying to write a simple test react-redux app to begin working with the gmail API, and right now state changes are not forcing a re-render.

What I have done:

I read this question. That person's problem seemed to be caused by not connect-ing the component, but as far as I know I am doing that. I read this question. That person's problem was caused by misunderstanding how combineReducers set objects on state, but I don't think I have the same misunderstanding. I'm sure I have some other one!

Here is my top level container, with routing:

'use strict'
import React from 'react'
import {Router, Route, IndexRedirect, browserHistory} from 'react-router'
import {render} from 'react-dom'
import {connect, Provider} from 'react-redux'

import store from './store'
import Home from './components/Home'
import Login from './components/Login'
import WhoAmI from './components/WhoAmI'
import NotFound from './components/NotFound'
import { handleClientLoad } from './gmailApi'

const ExampleApp = connect(
  ({ auth }) => ({ user: auth })
)(
  ({ user, children }) =>
    <div>
      <nav>
        {user ? <WhoAmI/> : <Login/>}
      </nav>
      {children}
    </div>
)

const onAppEnter = () => {
  handleClientLoad()
}

render(
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/" component={ExampleApp}>
        <IndexRedirect to="/home" />
        <Route path="/home" component={Home} onEnter={onAppEnter} />
      </Route>
      <Route path='*' component={NotFound} />
    </Router>
  </Provider>,
  document.getElementById('main')
)

Here is my code to render the component:

import React, { Component } from 'react'
import { handleAuthClick, handleSignOutClick } from '../gmailApi'
import store from '../store'
import { connect } from 'react-redux'


var authorizeButton = <button id="authorize-button" onClick={() => { handleAuthClick() }}>Authorize</button>
var signOutButton = <button id="signout-button" onClick={handleSignOutClick}>Sign Out</button>


const mapStateToProps = (state) => {
  return {
    labels: state.gmail.labels,
    isSignedIn: state.gmail.isSignedIn
  }
}

export const Labels = ({ isSignedIn, labels }) => {
  var button = isSignedIn ? signOutButton : authorizeButton
  console.log("labels ", labels)
  return (
    <div>
      {button}
      <ul>
        {labels && labels.map(label => <li>{label}</li>)}
      </ul>
    </div>
  )
}

export default class Home extends Component {

  constructor(props) {
    super(props)

    this.state = {labels: [], isSignedIn: false}
  }

  render() {
    return (
        <Labels labels={this.props.labels} isSignedIn={this.props.isSignedIn}/>
    )
  }

}

connect(mapStateToProps)(Home)

Here is my gmail reducer and action creators:

let initialState = {
    labels: [],
    isSignedIn: false
}

const reducer = (state=initialState, action) => {
    const newState = Object.assign({}, state);
  switch (action.type) {
  case SIGNED_IN:
    newState.isSignedIn = action.isSignedIn
    return newState
  case RECEIVE_LABELS:
    newState.labels = action.labels
    return newState
  }

  return state
}

const SIGNED_IN = 'SIGNED_IN'
export const signedIn = isSignedIn => ({
  type: SIGNED_IN, isSignedIn
})


const RECEIVE_LABELS = 'LABELS'
export const receiveLabels = labels => ({
    type: RECEIVE_LABELS, labels
})


export default reducer

Here is where I combine reducers:

import { combineReducers } from 'redux'

const rootReducer = combineReducers({
  auth: require('./auth').default, 
  gmail: require('./gmail').default
})
export default rootReducer

Here is a screenshot showing my console. (I don't really understand this cross-frame origin error and would welcome an explanation, but I assume it's ancillary to my problem since I get past it and succeed in passing an action through the reducer.) enter image description here

I succeed in console logging labels on the first render, but after labels is set on the state there is no rerender, and I don't console log labels again (or render the list of labels on the page).

Thanks for any help you can provide!

p.s. And, for completeness, here is where I'm doing async gmail api stuff. (I know I am not following the format of async action creators right now. I'm working with sample code from the gmail api and am just trying to get things up and running at first and then expand and clean up my code. I don't think the problem can possibly be in here because state gets populated fine; I just can't rerender the page.)

import store from '../store'
import { signedIn, receiveLabels } from '../reducers/gmail'
import gapi from './gapi'
import '../store'

var CLIENT_ID = '<stack overflow doesn't need to know this>';

// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"];

// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = "https://www.googleapis.com/auth/gmail.modify";


/**
 *  On load, called to load the auth2 library and API client library.
 */


export const handleClientLoad = function() {
    gapi.load('client:auth2', initClient);
}

/**
 *  Initializes the API client library and sets up sign-in state
 *  listeners.
 */
function initClient() {
    gapi.client.init({
        discoveryDocs: DISCOVERY_DOCS,
        clientId: CLIENT_ID,
        scope: SCOPES
    }).then(function () {
        // Listen for sign-in state changes.
        gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus)

        // Handle the initial sign-in state.
        updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());


    });
}

function updateSigninStatus(isSignedIn) {

        console.log(isSignedIn)
        store.dispatch(signedIn(isSignedIn))
}

/**
 *  Sign in the user upon button click.
 */
export const handleAuthClick = function(event) {
    console.log("got here")
    gapi.auth2.getAuthInstance().signIn();
    fetchLabels()
}

/**
 *  Sign out the user upon button click.
 */
export const handleSignoutClick = function(event) {
    gapi.auth2.getAuthInstance().signOut();
}

/**
 * Append a pre element to the body containing the given message
 * as its text node. Used to display the results of the API call.
 *
 * @param {string} message Text to be placed in pre element.
 */

/**
 * Print all Labels in the authorized user's inbox. If no labels
 * are found an appropriate message is printed.
 */
function fetchLabels() {
    console.log("gapi client ", gapi.client)
    gapi.client.gmail.users.labels.list({
        'userId': 'me'
    }).then(function (response) {
        var labels = response.result.labels;
        store.dispatch(receiveLabels(labels))
    })
}

Upvotes: 0

Views: 512

Answers (1)

whs.bsmith
whs.bsmith

Reputation: 406

An error I see is that you aren't exporting the connected component. You export the component, and then connect it later. The connect function returns a new Higher Order Component; it doesn't affect the component itself. Also, the connect function takes a second argument: the action creators that will dispatch the action to the state store. You need to import those actions if you want them to fire off to your reducers.

You have:

export default class Home extends Component {

  constructor(props) {
    super(props)

    this.state = {labels: [], isSignedIn: false}
  }

  render() {
    return (
        <Labels labels={this.props.labels} isSignedIn={this.props.isSignedIn}/>
    )
  }

}

connect(mapStateToProps)(Home)

just change it to:

import * as actions from './nameOfActionsFolder';

class Home extends Component {

  constructor(props) {
    super(props)

    this.state = {labels: [], isSignedIn: false}
  }

  render() {
    return (
        <Labels labels={this.props.labels} isSignedIn={this.props.isSignedIn}/>
    )
  }

}

export default connect(mapStateToProps, actions)(Home);

That way you will be exporting the connected component.

Upvotes: 1

Related Questions