Skip to content

Instantly share code, notes, and snippets.

@lancegliser
Last active April 24, 2019 02:50
Show Gist options
  • Select an option

  • Save lancegliser/9f415690ff0e41373ede88c425cd2220 to your computer and use it in GitHub Desktop.

Select an option

Save lancegliser/9f415690ff0e41373ede88c425cd2220 to your computer and use it in GitHub Desktop.
Publishing Redux state data through the Context API for React
// src/contexts/UserContext/IUser.ts
/**
* A representation of the user returned from the Microsoft Graph api.
*/
export default interface IUser {
"@odata.context"?: string, // "https://graph.microsoft.com/v1.0/$metadata#users/$entity"
displayName: string, // "Lance Gliser"
givenName?: string, // "Lance"
jobTitle?: string, // "Developer"
mail?: string, // "[email protected]"
mobilePhone?: string, // "816-726-2730"
preferredLanguage?: string, // null
surname?: string, // "Gliser"
userPrincipalName?: string, // "[email protected]"
id?: string, // "3bbce9ce-9ff2-4b32-8dd7-dcd215cbb783"
}
// A method of creating an object matching all of these properties without external code having to know too much.
export function getDefaultUser(): IUser {
let defaultUser = {} as IUser;
defaultUser.displayName = 'Guest';
return defaultUser;
}
// src/contexts/UserContext/IUserContext.ts
import IUser from "./IUser";
export default interface IUserContext {
user: IUser,
login: Function,
logout: Function,
}
// src/components/UserMenu/IUserMenu.ts
import IUserContext from "../../contexts/IUserContext";
export default interface IUserMenu {
userContext: IUserContext,
}
// src/contexts/UserContext/UserContextProvider.js
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import UserContext from '../../contexts/UserContext';
import {
clearAuthentication,
updateAuthenticationToken,
updateAuthenticationUser,
} from '../../actions';
import AzureServices from '../../utilities/AzureServices';
import MicrosoftGraphServices from '../../utilities/MicrosoftGraphServices';
import AppLoading from '../AppLoading/AppLoading';
const mapStateToProps = state => {
return {
user: state.user,
};
};
/**
* For the moment, this can not be used with hooks if it pulls redux state and router methods.
* @link https://github.com/reduxjs/react-redux/issues/1179
*/
class UserContextProvider extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
};
// A small delay to allow the variable below to be assigned before we call the callback.
// This might be refactored into componentDidMount, but I'm hoping for functional refactors sooner than later.
const delayedTokenReceivedCallback = (
errorDesc,
token,
error,
tokenType
) => {
setTimeout(() => {
this.tokenReceivedCallback(errorDesc, token, error, tokenType);
}, 5);
};
this.userAgentApplication = AzureServices.getUserAgentApplication(
delayedTokenReceivedCallback
);
}
//callback function for redirect flows
tokenReceivedCallback(errorDesc, token, error, tokenType) {
if (!token) {
throw new Error(error + ':' + errorDesc);
}
switch (tokenType) {
case 'id_token':
this.setState({ isLoading: true });
AzureServices.getAccessToken(
this.userAgentApplication,
token,
this.onLoginSuccess.bind(this),
this.onLoginFailure.bind(this)
);
break;
case 'access_token':
this.setState({ isLoading: true });
this.onLoginSuccess(token).bind(this);
break;
default:
throw new Error(`Unknown token type: ${tokenType}`);
}
}
/**
* @return void
*/
doLogin() {
const { history } = this.props;
this.setState({ isLoading: true });
history.push('/');
return AzureServices.getIdToken(this.userAgentApplication);
}
/**
* @param {String} accessToken
*/
onLoginSuccess(accessToken) {
this.setState({ isLoading: true });
const { dispatch, history } = this.props;
dispatch(updateAuthenticationToken(accessToken));
MicrosoftGraphServices.setAccessToken(accessToken)
.getCurrentUser()
.then(user => {
dispatch(updateAuthenticationUser(user));
this.setState({ isLoading: false });
history.push('/dashboard');
}, console.error);
}
/**
* @param error
*/
onLoginFailure(error) {
console.log(error);
}
/**
* Log out clearing redux stores
*/
doLogout() {
const { dispatch, history, user } = this.props;
if (!user.id) {
history.push('/');
return;
}
dispatch(clearAuthentication());
AzureServices.logout(this.userAgentApplication);
}
render() {
const { isLoading } = this.state;
const values = {
logout: this.doLogout.bind(this),
login: this.doLogin.bind(this),
user: this.props.user,
};
return (
<UserContext.Provider value={values}>
{isLoading ? <AppLoading /> : this.props.children}
</UserContext.Provider>
);
}
}
export default withRouter(connect(mapStateToProps)(UserContextProvider));
// src/components/UserMenu/UserMenu.tsx
import React, {EventHandler} from 'react';
import IUserMenu from "./IUserMenu";
const UserMenu: React.ComponentType<IUserMenu> = (props) => {
const { userContext } = props;
// All of your user properties are here, from Context.
const { user } = userContext;
const isLoggedIn = !!user.id;
// Annoying inbetween functions required to handling type negotations
const doLogin: EventHandler<any> = () => {
userContext.login();
};
// Annoying inbetween functions required to handling type negotations
const doLogout: EventHandler<any> = () => {
userContext.logout();
};
return (
(
isLoggedIn ?
<Button text="Logout" onClick={doLogout} />
:
<Button text="Login" onClick={doLogin} />
)
);
};
export default withUserContext(UserMenu);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment