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

these slides are be available at

http://tiny.cc/react-second-steps

Contact me at js@alxd.org

Second steps in React: auth

By Pawel Ngei

Second steps in React: auth

  • 4,224