second steps in react:
auth
all logos are copyrighted by their respective owners
all slides are licensed CC-BY-SA and code samples MIT
tiny.cc/react-second-steps
Paweł alxd Chojnacki
hacker and floss activist
digital nomad
full-stack JS dev with
learned my trade with
highly opinionated
batteries included (with ui-router)
created for medium and big SPAs
slow with large datasets
preferring custom magic to JS
two-way data-binding
now I work with
very little opinionated
just a V of MVC
created for components, not SPAs
blazing fast
embracing commonJS
one-way data-binding
React.js Ecosystem
React tutorials explain Components
multi-component views?
view-specific logic?
authentication?
import { Webpack, ES6 } from ('survive.js')
import React from ('react-tutorial')
import Router from ('react-router')
import Redux from ('redux-tutorial')
State of the App
/login
/
available with no auth token
available with an auth token
when auth token exists
when no auth token
when a request fails with 403
React Container / View Components
app/
actions/
components/
config/
reducers/
requests/
stores/
views/
index.js
...are regular components
{this.props.children}
componentWillReceiveProps()
just with
and
class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkCredentials(this.props.authToken)
}
componentWillReceiveProps (nextProps) {
this.checkCredentials(nextProps.authToken)
}
checkCredentials (authToken) {
(...)
}
render () {
return (
<div>
{this.props.children}
</div>
)
}
}
Simple Component
Pseudocode!
function requireAuthentication (Component) {
return class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkCredentials(this.props.authToken)
}
componentWillReceiveProps (nextProps) {
this.checkCredentials(nextProps.authToken)
}
checkCredentials (authToken) {
(...)
}
render () {
return (
<div>
{this.props.authToken
? <Component {...this.props} />
: null // You can add a spinner Component here
}
</div>
)
}
}
}
Higher Order Component
Pseudocode!
checkCredentials (authToken) {
if (!authToken) {
const storageToken = window.localStorage.getItem('authToken')
if (!storageToken) {
this.redirectToLogin()
} else {
this.props.setAuthToken(storageToken)
}
}
}
checkCredentials
import React from 'react'
import { connect } from 'react-redux'
export default function requireAuthentication (Component) {
class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkCredentials(this.props.authToken)
}
componentWillReceiveProps (nextProps) {
this.checkCredentials(nextProps.authToken)
}
checkCredentials (authToken) {
if (!authToken) {
const storageToken = window.localStorage.getItem('authToken')
if (!storageToken) {
this.redirectToLogin()
} else {
this.props.setAuthToken(storageToken)
}
}
}
redirectToLogin () {
this.context.router.push('/login')
}
render () {
return (
<div>
{this.props.authToken
? <Component {...this.props} />
: null // You can add a spinner Component here
}
</div>
)
}
}
(...)
const mapStateToProps = (state) => ({
authToken: state.auth.token
})
const mapDispatchToProps = (dispatch) => {
return {
setAuthToken: (authToken) => {
dispatch(setAuthToken(authToken))
}
}
}
return connect(
mapStateToProps,
mapDispatchToProps
)(AuthenticatedComponent)
}
with Redux
how to use them
<Router history={browserHistory}>
<Route path='/login' component={Login} />
<Route path '/' component={authenticatedComponent}>
<IndexRoute component={Dashboard}>
</Route>
</Router>
and
<Router history={browserHistory}>
<Route path='/login' component={Login} />
<IndexRoute component={requireAuthentication(Dashboard)}>
</Router>
Pseudocode!
index.js - real code
app/index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { Router, browserHistory, Route, IndexRoute } from 'react-router'
import { mainStore } from './stores/mainStore'
import requireAuthentication from './views/auth'
import Login from './views/login'
import Dashboard from './views/dashboard'
render(
<Provider store={mainStore}>
<Router history={browserHistory}>
<Route path='/login' component={Login} />
<IndexRoute component={requireAuthentication(Dashboard)}>
</Router>
</Provider>,
document.getElementById('app')
)
const mainReducer = combineReducers({
auth,
locale,
api: combineReducers({
login,
account,
clients
}),
views: combineReducers({
progressBar,
activeWidgets
})
})
and the Redux Reducer
app/actions/auth.js
const AUTH_SET_TOKEN = 'AUTH_SET_TOKEN'
const AUTH_DELETE_TOKEN = 'AUTH_DELETE_TOKEN'
export function setAuthToken (token) {
return {
type: AUTH_SET_TOKEN,
token
}
}
export function invalidateAuthToken () {
window.localStorage.removeItem('authToken')
return {
type: AUTH_DELETE_TOKEN
}
}
app/reducers/auth.js
import {
AUTH_SET_TOKEN,
AUTH_DELETE_TOKEN
} from '../actions/auth'
export default function auth (
state = {
token: undefined
}, action
) {
switch (action.type) {
case AUTH_SET_TOKEN:
return Object.assign({}, state, {
token: action.token
})
case AUTH_DELETE_TOKEN:
return Object.assign({}, state, {
token: undefined
})
default:
return state
}
}
requests and 403s
app/requests/data.js
import fetch from 'isomorphic-fetch'
import { invalidateAuthToken } from '../actions/auth'
import config from '../config'
export function getAuthenticatedData () {
return fetch(config.request_addres, {
method: 'GET'
}).then((response) => {
if (response.status === 403) {
invalidateAuthToken()
}
return response
})
}
Redux State propagation
<Router history={browserHistory}>
<Route path='/login' component={Login} />
<Route path='/' component={requireAuthentication(Navbar)}>
<Route path='/dashboard' component={Dashboard} />
<Route path='/clients' component={Clients}>
<IndexRoute component={NewClient}>
</Route>
</Route>
</Router>
componentWillReceiveProps (nextProps) {
this.checkCredentials(nextProps.authToken)
}
Pseudocode!
redirecting from login
checkCredentials (authToken) {
const storageToken = window.localStorage.getItem('authToken')
if (authToken || storageToken) {
this.props.setAuthToken(authToken || storageToken)
this.redirectToDashboard()
}
}
import React from 'react'
import { connect } from 'react-redux'
export default function requireUnauthenticated (Component) {
class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkCredentials(this.props.authToken)
}
componentWillReceiveProps (nextProps) {
this.checkCredentials(nextProps.authToken)
}
checkCredentials (authToken) {
const storageToken = window.localStorage.getItem('authToken')
if (authToken || storageToken) {
this.props.setAuthToken(authToken || storageToken)
this.redirectToDashboard()
}
}
redirectToDashboard () {
this.context.router.push('/dashboard')
}
render () {
return (
<div>
{this.props.authToken
? <Component {...this.props} />
: null // You can add a spinner Component here
}
</div>
)
}
}
(...)
const mapStateToProps = (state) => ({
authToken: state.auth.token
})
const mapDispatchToProps = (dispatch) => {
return {
setAuthToken: (authToken) => {
dispatch(setAuthToken(authToken))
}
}
}
return connect(
mapStateToProps,
mapDispatchToProps
)(requireUnauthenticated)
}
use view-specific logic in
container components to:
user permissions to component groups
auto-redirecting on 'global' level
keep auth data or user preferences
set and change locale
use Redux single store to
summing it up
views can be implemented as components
with Redux componentWillReceiveProps works wonders
use single Redux store per App
it's handy to create higher order components