auth and views
in React.js

an intro to

all logos are copyrighted by their respective owners

all slides are licensed CC-BY-SA and code samples MIT

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-router

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 tutorials explain Components

What about multi-component views?

How to implement view-specific logic?

How to authenticate React App?

React Container 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>
      )
    }
  }
}

Wrapper Component

Pseudocode!

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>

or

  <Router history={browserHistory}>
    <Route path='/login' component={Login} />
    <IndexRoute component={requireAuthentication(Dashboard)}>
  </Router>

Pseudocode!

index.js with routing

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')
)

use single store!


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)
}

putting advanced view-specific logic in
container components

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

map full state, not its properties to props

it's handy to create wrappers

code samples soon at
https://alxd.org/

auth and views in react.js

By Pawel Ngei

auth and views in react.js

  • 2,095