I'd normally throw a question like this on stackoverflow, but I suspect the problem is something I'm missing in the structure of redux elements across files in this project.
I am creating a component that calls Firebase's Google login popup. After writing the action creators & component, login worked fine. However, after trying to write the reducers, the action creators (thunks, all) aren't getting past their respective return statements.
I've tried reverting the changes, but I'm pretty new & not awesome w/ VCS.
so: Is there a straightforward way to debug a situation like this? is there another file i should've made changes to?
a few details
duck initializes w/ initial stateconsole.log() statements work if they're in an action creator, but before the return statement. if they're in a thunk, nada(and pls. feel free to point out ill-advised patterns or dumb stuff i'm doing. always looking to get better)
// containers/LoginWidget.jsx (mistakenly combined container/presentation component, plan on splitting once it's working)
import React from 'react';
import Paper from 'material-ui/Paper';
import RaisedButton from 'material-ui/RaisedButton';
import {attemptLogin, logUserOut} from 'containers/LoginWidget/modules/user-auth.js';
import {connect} from 'react-redux';
class LoginWidget extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Paper style={styles.wrapper}>
<RaisedButton
label="Sign in with Google"
labelStyle={styles.loginButton}
backgroundColor={'#D84B37'}
icon={<img height={15} src={require('./assets/google.png')}/>}
name="google"
onTouchTap={() => this.props.dispatch(attemptLogin)}
/>
<RaisedButton
label="Logout"
labelStyle={styles.loginButton}
backgroundColor={'#D84B37'}
name="logout"
onTouchTap={() => this.props.dispatch(logUserOut)}
/>
</Paper>
)
}
}
export default connect()(LoginWidget);
// containers/LoginWidget/modules/user-auth.js
// ------------------------------------
// Constants
// ------------------------------------
//user authentication statuses
export const USER_NOT_AUTHENTICATED = 'USER_NOT_AUTHENTICATED'; //ie, just arrived to site & hasn't logged in yet
export const USER_AUTH_IN_PROGRESS = 'USER_AUTH_IN_PROGRESS';
export const USER_LOGGED_IN = 'USER_LOGGED_IN';
export const USER_AUTH_ERROR = 'USER_AUTH_ERROR';
export const USER_LOGGED_OUT = 'USER_LOGGED_OUT';
// ------------------------------------
// Actions
// ------------------------------------
export const startAuthListener = function () {
//this listener will update state upon changes of auth status.
console.log("auth listener function started"); //this prints
return dispatch => {
console.log("auth listener actn crtr started"); //this doesn't print
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
// User is signed in, dispatch action
dipatch({type: USER_LOGGED_IN, authData: user})
} else {
// User is signed out, dispatch action
dispatch({type: USER_LOGGED_OUT})
}
});
}
}
export const attemptLogin = function () {
console.log("attemptLogin fired")
return dispatch => {
console.log("login actn crtr started");
//using globally avail firebase
let provider = new firebase.auth.GoogleAuthProvider();
//specify data we want from google oauth
provider.addScope('profile, email');
//create login popup
firebase.auth().signInWithPopup(provider).then(function (result) {
//the auth listener in startAuthListener() will handle the rest
}
).catch(function (error) {
//something's amiss!
dispatch({type: USER_AUTH_ERROR, error: error})
});
}
}
export const logUserOut = function () {
return dispatch => {
firebase.auth().signOut().then(function () {
dispatch({type: USER_LOGGED_OUT})
});
}
}
export const actions = {
startAuthListener,
attemptLogin,
logUserOut
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[USER_NOT_AUTHENTICATED]: (state, action) => Object.assign({}, state, {authStatus: USER_NOT_AUTHENTICATED}),
[USER_AUTH_IN_PROGRESS]: (state, action) => Object.assign({}, state, {authStatus: USER_AUTH_IN_PROGRESS}),
[USER_LOGGED_IN]: (state, action) => Object.assign({}, state, {authStatus: USER_LOGGED_IN, authData: action.authData}),
[USER_AUTH_ERROR]: (state, action) => Object.assign({}, state, {authStatus: USER_AUTH_ERROR, authError: action.error}),
[USER_LOGGED_OUT]: (state, action) => Object.assign({}, state, {authStatus: USER_LOGGED_OUT}),
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {authStatus: USER_NOT_AUTHENTICATED}
export default function authReducer(state = initialState, action) {
console.log(state)
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
// store/reducers.js
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import authReducer from 'containers/LoginWidget/modules/user-auth.js'
export const makeRootReducer = (asyncReducers) => {
return combineReducers({
// Add sync reducers here
router,
authReducer,
...asyncReducers
})
}
export const injectReducer = (store, { key, reducer }) => {
store.asyncReducers[key] = reducer
store.replaceReducer(makeRootReducer(store.asyncReducers))
}
export default makeRootReducer
You have to call your action creator to _return_ the thunk, then dispatch that. redux-thunk looks at an action and checks to see if it's a function, and, if so, calls it with dispatch and getState. When you do:
onTouchTap={() => this.props.dispatch(attemptLogin)}
You are dispatching the _attemptLogin_ function itself, not the anonymous dispatch function it returns. What's happening is that redux-thunk is seeing your attemptLogin function, assuming it is a thunk, and calling it... and then it, of course, just ends at that first return statement. What you want to do is provide it that internal function that accepts the dispatcher.
This would look like:
onTouchTap={() => this.props.dispatch(attemptLogin())} // notice that we invoke your action creator
So now the thing that gets dispatched is _still a function_, but it's your anonymous thunk awaiting the dispatcher (which will be provided by redux-thunk). Does that make sense?
1) You dispatch an action, in this case your action is just a function awaiting a dispatcher.
2) Redux-thunk realizes that this is not a normal action, but is a function, so it calls it with dispatch and getState.
3) The rest of your action creator runs and is able to asynchronously dispatch as many actions as you want.
ahh beautiful, works like a charm. i'd wrapped the dispatches in anon functions for binding purposes w/o even considering how that might affect the action creators.
thanks very much, @davezuko !
You are still able to wrap the functions in another, just like you were doing, you just have to make sure you are calling them correctly. For example, an async function using redux-thunk could very well look like:
const fetchSomethingById = (id) => {
return (dispatch) => {
// dispatch your actions
}
}
Which is callable as such:
this.props.dispatch(fetchSomethingById(1))
You can eliminate potential mistakes by using mapDispatchToProps's action creator binding:
connect((state) => { ... }, {
fetchSomethingById,
})
// calling this will dispatch the result of this function... which happens to be another
// function. Redux-thunk middleware notices that what was dispatched is actually a function,
// and will call that second function, thereby executing your async code.
this.props.fetchSomethingById(1)
I noticed that in /routes/Counter/containers/CounterContainer.js, there's some relevant syntax I couldn't figure out:
const mapActionCreators = {
increment: () => increment(1),
doubleAsync
}
/* ... */
export default connect(mapStateToProps, mapActionCreators)(Counter)
I wasn't able to find much documentation on passing a mapActionCreators function to connect().
Is this another way to allow for the direct invoking of action creators (i.e., sans dispatch())?
Ignore the naming, it's mapDispatchToProps. This might help: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options.
I just wanted to share a beginner's perspective on this:
@brandonmp
I wasn't able to find much documentation on passing a mapActionCreators function to connect().
@davezuko
Ignore the naming, it's mapDispatchToProps. This might help: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options.
The naming really threw me for a loop while learning to use this starter kit. Even if mapActionCreators makes sense in this case, newer users are looking for familiar names, and renaming mapDispatchToProps here causes a lot of confusion.
@quinnkeast just updated the naming with https://github.com/davezuko/react-redux-starter-kit/commit/914af6e51b74ac1356dcf3229dba4c7483a6ca39. This naming was introduced in a PR that must have been a stylistic quirk of the submitter that I just never caught/took the time to change. Thanks for making the argument again, hopefully this saves some other people the headache of deciphering the naming.
Fantastic. Thank you!
Most helpful comment
Ignore the naming, it's
mapDispatchToProps. This might help: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options.