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.