I have an async actionCreator which handle my app's authentication flow:
function createAuthenticationResponse(err, grant) {
return {
type: AUTHENTICATION_RESPONSE,
payload: err || grant,
error: Boolean(err)
}
}
function authenticate() {
// return a thunk.
return dispatch => {
// Notify the system that we are authenticating.
dispatch({ type: AUTHENTICATE });
// Trigger the auth flow.
myAuthModule.authorize((err, grant) => {
// Trigger a state-change on the outcome.
dispatch(createAuthenticationResponse(err, grant));
// Q: How do I handle this side-effect?
if (!err) {
dispatch(extractUserInfo(grant));
}
});
};
}
My actionCreator contains business logic to extract the user info from the grant if the user was succesfully authenticated; should this logic live in my action creator? If not, where should I place it, inside my reducer?
In other architectures I would bind a command to trigger on AUTHENTICATION_RESPONSE; but this doesn't feel like a middleware job?
I think what you're suggesting is totally sensible.
You can use Redux Thunk both for control flow and side effects.
You should never put side effects into the reducers.
Related
I am using SolidJS and building a SPA (no server rendering). For authentication, I use the #aws-amplify/core and #aws-amplify/auth packages. At the application root I call the Hub.listen function:
Hub.listen('auth', ({ payload }) => console.log(payload));
In the SignUp component I call Auth.federatedSignIn:
const SignUp = () => {
return (
<button onClick={() => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google });
}}>
Sign up
</button>
);
}
I have configured the Amplify as such:
Amplify.configure({
Auth: {
region: import.meta.env.VITE_AWS_REGION,
userPoolId: import.meta.env.VITE_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_AWS_POOL_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_AUTH_URL,
responseType: 'code',
redirectSignIn: location.origin + '/account/external',
redirectSignOut: location.origin + '/my',
},
},
});
When I click on the button I am redirected to the import.meta.env.VITE_AUTH_URL (simply outside of my app), choose an account, and then return back to the /account/external page. At that time I expect a consoled payload object in Web tools, but there is nothing. I get it when I call Auth.signOut(), so I assume that I configured Amplify correctly and Hub is subscribed to the auth channel.
My thoughts were that Hub cannot catch any events because after returning the application basically renders again in a new context and Hub simply isn't able to catch anything (events aren't sent from AWS?). I tried to declare the urlOpener function under the oauth property in the config and Google's sign page opened in a new tab, but even then I couldn't get any events in the preserved old page (from which I called Auth.federatedSignIn).
Questions:
How should I organize the code to get the signIn and signUp events?
Can I pass some data into the Auth.federatedSignIn to get it back in the Hub.listen, so I will be able to join the CognitoUser with the data that existed at the time of starting Sign in/Sign up (I want to add a new login type to existed user)?
Here is an example regarding the first question. Just check that your listener is set before you call the Auth.federatedSignIn() method.
export default class SignInService {
constructor(private landingFacade: LandingFacade) {
this.setupAuthListeners(); // Should be called at the top level.
}
private setupAuthListeners() {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
this.landingFacade.signInSuccess();
break;
case 'signIn_failure':
console.log('Sign in failure', data);
break;
case 'configured':
console.log('the Auth module is configured', data);
}
});
}
public async signIn(): Promise<void> {
await Auth.federatedSignIn();
}
}
For the second one: I'll use a local state and set/query the object you need.
I have an app with different 'procedures' (think posts or pages), which one can like. Currently the process works: Tap like => run method "likeProcedure" => run dispatch action "likeProcedure" => update UI. It usually happens almost immediately, but sometimes there's a lag that gives this a "non-native" feel. Is there some sort of way that I could return feedback immediately, while stile holding single origin of truth on the firebase database?
Thank you!
Page Code:
<v-icon
v-if="!userProfile.likedProcedures || !userProfile.likedProcedures[procedure.id]"
color="grey lighten-1"
#click="likeProcedure({ id: procedure.id })"
>
mdi-star-outline
</v-icon>
and
computed: {
...mapState(["userProfile"]),
procedures() {
return this.$store.getters.getFilteredProcedures();
},
},
Vuex code:
async likeProcedure({ dispatch }, postId) {
const userId = fb.auth.currentUser.uid;
// update user object
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: true,
});
dispatch("fetchUserProfile", { uid: userId });
},
Side note: I'm trying to remove the dispatch("fetchUserProfile") command, but this doesn't work, because then I'm calling dispatch without using it. And I cannot remove dispatch because then the object calling it is empty. And I cannot remove the object, because then the argument ('postId') isn't working. So if anyone knows how to deal with that, that would be extremely helpful.
Thank you :)
So this is the best solution I've come up yet. It kind of destroys the idea of a single source of truth, but at least it provides an immediate UI update:
async likeProcedure({ dispatch, state }, postId) {
console.log("likeProcedure");
const userId = fb.auth.currentUser.uid;
// line below provides immediate update to state and hence to the UI
state.userProfile.likedProcedures[postId.id] = true;
// line below updates Firebase database
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: state.userProfile.likedProcedures[
postId.id
],
});
// line below then fetches the updated profile from Firebase and updates
// the profile in state. Kind of useless, but ensures that client and
// Firebase are in-sync
dispatch("fetchUserProfile", { uid: userId });
},
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
commit("setUserProfile", userProfile.data());
// change route to dashboard
if (router.currentRoute.path === "/login") {
router.push("/");
}
},
Please forgive me if this is an easy answer. I have a complicated login logic that requires a few calls before a user has a complete profile. If a step fails, it shouldn't break the app -- the user just doesn't get some supplemental information.
The flow I'm looking to achieve is this:
Call Revalidate.
Revalidate calls RevalidateSuccess as well as ProfileGet (supplemental fetch to enhance the user's state).
ProfileGetSuccess.
To save tons of code, the actions exist (it's a giant file).
The app kicks off the action: this._store.dispatch(new Revalidate())
From there, we have the following effects:
#Effect()
public Revalidate: Observable<any> = this._actions.pipe(
ofType(AuthActionTypes.REVALIDATE),
map((action: Revalidate) => action),
// This promise sets 'this._profile.currentProfile' (an Observable)
flatMap(() => Observable.fromPromise(this._auth.revalidate())),
// Settings are retrieved as a promise
flatMap(() => Observable.fromPromise(this._settings.get())),
switchMap(settings =>
// Using map to get the current instance of `this._profile.currentProfile`
this._profile.currentProfile.map(profile => {
const onboarded = _.attempt(() => settings[SettingsKeys.Tutorials.Onboarded], false);
return new RevalidateSuccess({ profile: profile, onboarded: onboarded });
}))
);
//Since I couldn't get it working using concatMap, trying NOT to call two actions at once
#Effect()
public RevalidateSuccess: Observable<any> = this._actions.pipe(
ofType(AuthActionTypes.REVALIDATE_SUCCESS),
mapTo(new ProfileGet)
);
#Effect()
public ProfileGet: Observable<any> = this._actions.pipe(
ofType(AuthActionTypes.PROFILE_GET),
// We need to retrieve an auth key from storage
flatMap(() => Observable.fromPromise(this._auth.getAuthorizationToken(Environment.ApiKey))),
// Now call the service that gets the addt. user data.
flatMap(key => this._profile.getCurrentProfile(`${Environment.Endpoints.Users}`, key)),
// Send it to the success action.
map(profile => {
console.log(profile);
return new ProfileGetSuccess({});
})
);
Reducer:
export function reducer(state = initialState, action: Actions): State
{
switch (action.type) {
case AuthActionTypes.REVALIDATE_SUCCESS:
console.log('REVALIDATE_SUCCESS');
return {
...state,
isAuthenticated: true,
profile: action.payload.profile,
onboarded: action.payload.onboarded
};
case AuthActionTypes.PROFILE_GET_SUCCESS:
console.log('PROFILE_GET_SUCCESS');
return { ...state, profile: action.payload.profile };
case AuthActionTypes.INVALIDATE_SUCCESS:
return { ...state, isAuthenticated: false, profile: undefined };
default:
return state;
}
}
As the title mentions, dispatching the action runs infinitely. Can anyone point me in the right direction?
The answer lies here:
this._profile.currentProfile.map needed to be this._profile.currentProfile.take(1).map. The issue wasn't the fact that all my actions were being called, but because I was running an action on an observable, I suppose it was re-running the action every time someone was touching the observable, which happened to be infinite times.
Moreso, I was able to refactor my action store so that I can get rid of my other actions to call to get the rest of the user's data, instead subscribing to this._profile.currentProfile and calling a non-effect based action, ProfileSet, when the observable's value changed. This let me remove 6 actions (since they were async calls and needed success/fail companion actions) so it was a pretty big win.
I'm creating a Redux middleware that listens for a specific action. If that action type matches what I'm looking for, I want to dispatch another action. The reason for this is because I have different components with some shared functionality, so I want to update actions to have similar types, but different payloads (term), like so:
const updateActions = store => next => action => {
console.log(action);
if (action.type === 'UNIQUE_ACTION_A') {
return next({ type: 'NEW_ACTION', term: 'new_action_a_test' });
} else if (action.type === 'UNIQUE_ACTION_B') {
return next({
type: 'NEW_ACTION',
term: 'new_action_b_test'
});
}
return next(action);
};
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk, updateActions))
);
The problem I'm having is that these actions are not dispatching. When I console.log all actions, they continue to run as normal, rather than dispatching the new actions. It's as if the call to dispatch these actions are just being ignored. What am I doing incorrectly here? Thanks!
There's a difference between next and dispatch inside middleware. dispatch sends an action to the very start of the dispatch chain, which will cause it to run through all the middleware in the pipeline. next sends it to the next middleware after this one, and eventually to the reducers. By calling next({type : "NEW_ACTION"}), you're sending it to the next middleware, and this middleware will never see "NEW_ACTION".
Also see the Redux FAQ entry on next vs dispatch in middleware.
I'm writing a piece of Redux Middleware which is responsbile for adding the user's OAuth AccessToken to API_CALL cations before they hit the redux-api-middleware.
// sign appends the `Authorization` HTTP Header to an API_CALL action
function sign(action) {
action[CALL_API].headers = {
...action[CALL_API].headers,
Authorization: `Bearer ${getState()auth.accessToken}`;
}
}
// Redux middleware signature.
return ({dispatch, getState}) => {
return next => action => {
if (action[CALL_API]) {
sign(action);
}
return next(action);
}
}
However, I also want this middleware to detect when the User's AccesToken has expired...
function tokenExpired() {
return (Date.now() > getState().auth.expirationTime);
}
When this happens, the middleware detains the action (prevents it from being passed to the next middleware in the chain) and stores it in an internal list. It then kicks off the async. Token refresh process by dispatching a 'refresh access token' FSA:
if (tokenExpired()) {
// detainActions is an array, declared outside
// of the middleware's scope.
detainActions.push(action);
dispatch(refreshAccessToken());
}
else {
next(sign(action));
}
Finally, I want to listen for when the 'refresh access token' flow has completed and flush (re-dispatch) all detained actions. Currently I am doing this by looking out for AUTHENTICATION_RESPONSE FSA's as they flow through my middleware (the AUTHENTICATION_RESPONSE action is dispatched as as side-effect of the refreshAccessToken thunk.
// Redux middleware signature.
return ({dispatch, getState}) => {
return next => action => {
if (action.type === AUTHENTICATION_RESPONSE && !action.error) {
// Let the AuthResponse action pass to the store.
next(action);
// Flush detained actions now we have a new grant.
return flushAndRedispatchDetainedActions(dispatch);
}
else {
// Sign CALL_API requests logic as above.
}
}
}
However I am not happy with this approach as there is no certainty that the AUTHENTICATION_RESPONSE FSA will actually hit the reducers (it may be intercepted by other middleware, or be further defered).
Possible alternative approaches I have considered are having the refreshAccessToken actionCreator return a thunk which returns a Promise; that way my middleware can wait for that promise to resolve before flushing and replaying all requests, ie:
if (tokenExpired()) {
// refreshAccessToken thunk returns a Promise.
dispatch(refreshAccessToken());
.then(flushAndRedispatchDetainedActions();
}
Or alternativley I could have my middleware observe the store directly and trigger an action when the auth.accessToken value changes - however, I'm not clear on what the guidance is for middleware observing the store (I'm guessing it's not possible as middleware needs to be instantiated before the final store object is created.)
Thanks :)
Update
Thinking about the problem on the way home; if I were to move the actual authentication logic out of refreshAccessToken (a thunk'd action creator), and into the middleware itself then I would save a lot of pain:
// Redux middleware signature.
return ({dispatch, getState}) => {
return next => action => {
if (action[CALL_AUTH]) {
authHelper.refreshGrant()
.then(grant => dispatch(processGrant(newGrant));
}
else {
// Sign CALL_API requests logic as above.
}
}
}