I recently refactored the first React component I wrote. My code was something like this (It wasn’t really a Signup screen, but there’s a similarity in the workflow):
const db = require('./db.js')
const SignupScreen = React.createClass({
getInitialState: function(){
return { User: this.props.User, message: '' }
},
render: function(){
const User = this.state.User
const message = this.state.message
return (<div>
Username: ...<br>
Password: ...<br>
<input type="submit"
value="Signup"
onclick={this.handleSignup}
/>
<p>{message}</p>
</div>)
},
handleSignup: function(event){
const User = this.state.User
this.checkUsernameIsUnique(User.getUserName())
.then(isUnique => {
if(isUnique){
this.saveUser()
.then(response =>
this.setState({message: 'Account Created'})
)
.catch(error =>
this.setState({message: 'Could not save Account, please try later'})
)
} else {
this.setState({message: 'Username taken'})
}
})
},
checkUsernameIsUnique: function(){
return db.find('user', this.state.User.toPojo())
.then(user => user? true : false)
},
saveUser: function(){
return db.save('user', this.state.User.toPojo())
}
})
So this is pretty bad, but also quite similar to the example code in the React docs, so possibly other people have written stuff like this too.
Things to note:
User
prop
is an object with custom getters, setters, etc, adding complexity to the code, and tedium to unit-test setup. When I changed User
's constructor signature a bit, the setup in my tests broke, even though what they were testing didn’t.require('./db.js')
make it awkward to test. Sure, you can mock them with rewire
, but if you have to magically rewrite your code just for the tests, it’s hard to pretend you wrote test first like a proper programmer.The refactor:
//signup.jsx
const SignupScreen = React.createClass({
getInitialState: function(){
return { User: this.props.User, message: '' }
},
render: function(){
const User = this.state.User
const message = this.state.message
const {handleSignup} = this.props
const setMessage = message => this.setState({message})
return (<div>
Username: ...<br> //updates User.username
Password: ...<br> //updates User.password
<input type="submit" value="Signup" onclick="{(event)=">{
handleSignup(setMessage, User)
}}/>
<p>{message}</p>
</div>)
})
//signup-actions.js
export const handleSignup = (findUser, saveUser) =>
(setMessage, user) => {
findUser(user.username)
.then(dbUser => {
return dbUser?
setMessage("Username taken")
: saveUser(user)
.then(_ => setMessage("Account created"))
}).catch(e => setMessage("Couldn't create account "))
}
//signup-page.js
import SignupScreen from './signup.jsx'
import {handleSignup} from './signup-actions.js'
import db from './db.js'
const User = {username: '', password: ''}
const saveUser = user => db.save('users', user)
const findUser = username => db.find('users', {username})
ReactDOM.render((<SignupScreen user={User} handlesignup={handleSignup(findUser, saveuser)}
, document.getElementById('container'))
There is probably still a lot to improve upon here, but there are at least some improvements:
SignupScreen
, we just pass in plain object literal.SignupScreen
). All it knows to do is create a button that calls a function I’ve passed in.SignupScreen
has simple arguments, no implicit dependencies (except React), and is easy to instantiate in different environments (eg: tests).So my conclusions are:
Written by Keith Alexander. Interests include functional programming, web tech, event sourcing and linked data.