From Angular to React

If you ever looked at my github – public or private you can see that I have settled on a personal template for Angular applications. While I’m sure that there are things that are against some blog post of conventions, it works and gets lots of jobs done. After digging into the performance of our SPA at Tubular I found that we were spending 1.5 seconds rendering the results of an XHR (Ajax) requests to fetch some data.

Yes, our Angular application was taking 1.5 seconds to render 15 result items and a list (about 15 facts) of the search each having about 5 items. That’s totally crazy. Like anybody you crack open the blog posts on performance and read and fix and read and fix until you realize that this is a huge undertaking that has to do with the way that data is bound in Angular and it’s really easy to get an application working it really hard to get the performance back if you didn’t take care of it from day one.

Enter React

What I found is that a few people encourage the use of React to take over the rendering from Angular. Learning React is something that I’ve been putting off for a while, I built one quick React-native application but will admit that I didn’t fully understand what I was working with. So, I took my latest project (not public) and started recoding the application from Angular to React+Redux.

Learnings

There are way too many blog posts take build the ToDo application, or overly simple React applications that don’t end up with all of the real problems that an application has.

For starters –

Login Content

Is a fairly complicated React application, it involves:

  • React
  • React-Router
  • Redux
  • AJAX (via axios in my case)
  • Validation
  • Build system (something to generate app.js from all of the files)
  • Plus a bunch of other things (Material-UI to make it look pretty)

Once you have it all working, it looks pretty simple and it does make sense when you read the code.  Some key snippets:

Login.jsx — This is the react View

  1import React, { Component, PropTypes } from 'react'
  2import { TextField, Checkbox, FlatButton, RaisedButton, FontIcon } from 'material-ui';
  3import { Link } from 'react-router';
  4import { AuthBox, AuthHeader, AuthBody, AuthError } from './AuthBox'
  5import validator from 'validator'
  6import { resetLogin, loginUser, logoutUser } from './actions'
  7
  8export default class Login extends Component {
  9    static contextTypes = {
 10        dispatch: PropTypes.func.isRequired,
 11        store: PropTypes.object.isRequired,
 12    }
 13
 14    constructor() {
 15        super()
 16
 17        this.state = {
 18            email: undefined,
 19            password: undefined,
 20            remember: true,
 21        }
 22
 23        this._submit = this._submit.bind(this)
 24        this._set_remember = this._set_remember.bind(this)
 25        this._set_email = this._set_email.bind(this)
 26        this._set_password = this._set_password.bind(this)
 27    }
 28
 29    componentWillMount() {
 30        this.context.dispatch(resetLogin())
 31        // If you are on the login page, lets clear the state to make sure you can login
 32        this.context.dispatch(logoutUser())
 33    }
 34
 35    _submit(event) {
 36        event.preventDefault()
 37
 38        var next = this.props.location.query.next
 39
 40        this.context.dispatch(loginUser({
 41                email: this.state.email, 
 42                password: this.state.password, 
 43                remember: this.state.remember
 44            }, next))
 45    }
 46
 47    // Checkbox to remember this user
 48    _set_remember(event, isInputChecked) {
 49        this.setState({remember: isInputChecked})
 50    }
 51
 52    _set_email(event) {
 53        this.setState({email: event.target.value})
 54    }
 55
 56    _set_password(event) {
 57        this.setState({password: event.target.value})
 58    }
 59
 60    _validate() {
 61        var valid = true
 62
 63        var update = {
 64            ready: false,
 65            email_error: undefined,
 66            password_error: undefined,
 67        }
 68
 69        if (this.state.email != undefined) {
 70            var email = validator.trim(this.state.email)
 71            if (email.length == 0) {
 72                update.email_error = "Please enter your email"
 73                valid = false
 74            } else if (!validator.isEmail(email)) {
 75                update.email_error = "Invalid email address"
 76                valid = false
 77            }
 78        } else {
 79            valid = false
 80        }
 81
 82        if (this.state.password != undefined) {
 83            var password = validator.trim(this.state.password)
 84            if (password.length < 8) {
 85                update.password_error = "Passwords must be at least 8 characters"
 86                valid = false
 87            }
 88        } else {
 89            valid = false
 90        }
 91
 92        update.ready = valid
 93        return update
 94    }
 95
 96    render() {
 97        const valid = this._validate()
 98        console.log(valid)
 99        
100        return (
101            <AuthBox title="Login">
102      <AuthBody>
103        <div data-layout="row" data-layout-align="center">
104          <RaisedButton 
105              href="/api/v1/oauth.redirect?provider=google" 
106              secondary={true} 
107              target="_self" 
108              className="margin-left-0 margin-right-0 margin-top-10 margin-bottom-10" 
109              label="Sign in with Google" />
110          <span data-flex="5"></span>
111          <RaisedButton 
112              href="/api/v1/oauth.redirect?provider=github" 
113              secondary={true} 
114              target="_self" 
115              className="margin-left-0 margin-right-0 margin-top-10 margin-bottom-10" 
116              label="Sign in with GitHub" />
117        </div>
118
119        <form onSubmit={this._submit}>
120          <TextField 
121              onChange={this._set_email} 
122              className="margin-vertical-10" 
123              fullWidth={true} 
124              floatingLabelText="Email address" 
125              errorText={valid.email_error} />
126          <TextField 
127              onChange={this._set_password} 
128              className="margin-vertical-10" 
129              fullWidth={true} 
130              floatingLabelText="Password" 
131              type="password" 
132              errorText={valid.password_error} />
133          <div data-layout="row" data-layout-align="space-between center" className="margin-vertical-10">
134            <div data-flex>
135              <Checkbox onCheck={this._set_remember} defaultChecked={this.state.remember} label="Remember Me" />
136            </div>
137            <div>
138              <FlatButton containerElement={<Link to="/app/auth/forgot" />} label="Forgot Password" primary={false}/> 
139            </div>
140         </div>
141
142          <div className="margin-vertical-10">
143            <RaisedButton 
144                type="submit" 
145                onClick={this._submit} 
146                className="full-width" 
147                disabled={!valid.ready} 
148                label="Log In" primary={true}/> 
149          </div>
150          <div className="margin-vertical-10">
151            <FlatButton 
152                containerElement={<Link to="/app/auth/register" />} 
153                className="full-width" 
154                label="Register your account" 
155                primary={true}/> 
156          </div>
157        </form>
158      </AuthBody>
159    </AuthBox>
160        )
161    }
162}

from actions.js — what I find interesting is that the best place to save the authentication token is from here.

 1export function loginUser(creds, next) {
 2    return dispatch => {
 3        dispatch(requestLogin(creds))
 4
 5        axios.post('/api/v1/auth.login', {email:creds.email, password:creds.password})
 6             .then((response) => {
 7                let resp = response.data
 8                if (resp.ok) {
 9                    _saveToken(resp.token, creds.remember)
10                    dispatch(receiveLogin(resp.token))
11                    browserHistory.push(next ? next : DEFAULT_HOME_PAGE)
12                } else {
13                    dispatch(loginError(resp.error))
14                }
15             }).catch((response) => {
16                dispatch(loginError("internal error"))
17             })
18    }
19}

Conclusion

There is a big shift in thinking from Angular to React. The biggest is what the state is, this is compounded by using Redux. Where your controller actions are now moved to actions.js and reducer.js.  So far for most of my cases the reducer feels pretty trivial:

1    case T.LOGIN_SUCCESS:
2        return { ...state, 
3                 isFetching: true, 
4                 isAuthenticated: true, 
5                 errorMessage: undefined }

It’s possible to totally understand the route that an application is taking, though it still requires a bit of work to change thinking from all-in-one controller to having the logic spread across three files.

Is this easier to rationalize about a program, maybe, maybe not.  It’s clear where the state is and that all state changes are initiated in actions.js which can be shared and re-used quickly. Would this be solved if people just designed good programs to start with, probably, but I understand the value of creating these abstractions to force thinking.