😴
Fighting Javascript Fatigue
“Extreme tiredness caused by library churn in Github’s most active community”
Fatigue strikes down many… 💤
How can we protect our products? 💂
.NET MVC + Razor templates
Migrating to micro service backend 🐜
Frontend was busy building Cathedrals ⛪️
Monolith Knockout or Angular SPAs
Products were tightly
coupled to frameworks
Separate applications for 🖥 and 📱
Every rebuild was essentially 👶🛁💦
Hackathon #1 🚀
Node server,
Universal React,
Responsive web,
SEO
Some time the following week...
“How can we protect ourselves
against this churn?”
What hurts the most when rebuilding? 🤕
Success would mean
decoupling all of these
All of these needed to
outlive any framework…
What if we could npm install
our business rules?
Focused 👁
Vanilla 🍦
Tested ✅
Pure functions 😇
import { refine } from 'seek-refine-job-search';
const params = {
keywords: 'javascript',
salaryFrom: 60000
};
const refinements = refine(params);
{
…
salary: {
to: [
{ label: '$30k', value: '30000', isActive: false },
{ label: '$40k', value: '40000', isActive: false },
{ label: '$50k', value: '50000', isActive: false },
{ label: '$60k', value: '60000', isActive: true },
{ label: '$70k', value: '70000', isActive: false, refineParams: { … },
{ label: '$80k', value: '80000', isActive: false, refineParams: { … },
{ label: '$100k', value: '100000', isActive: false, refineParams: { … },
{ label: '$120k', value: '120000', isActive: false, refineParams: { … },
{ label: '$150k', value: '150000', isActive: false, refineParams: { … },
{ label: '$200k', value: '200000', isActive: false, refineParams: { … },
{ label: '$200k+', value: '999999', isActive: false, refineParams: { … } }
]
}
…
}
{
label: '$100k',
value: '100000',
isActive: false,
refineParams: {
keywords: 'javascript',
salaryFrom: 60000,
salaryTo: 100000
}
}
Pure functions are ideal
for unit testing
const cases = [
{
input: { … },
output: { … },
should: 'given input should yield output'
},
…
];
import { refine } from 'seek-refine-job-search';
describe('Salary refinement', () => {
cases.forEach(({ input, output, should }) => {
it(should, () => {
expect(refine(input)).to.deep.equal(output);
});
});
});
How could we prove our
approach to the business?
Demo to stakeholders without CSS 🚫💄
Clarified what is a business rule
and what is the experience.
Enormous flexibility with the UX
Like building a WinAmp skin ⚡️
Frameworks 😻 pure functions
import { refine } from 'seek-refine-job-search';
// Pass query, receive refinement view model
const refinements = refine(getCurrentQuery());
render() {
return <RefinePanel refinements={refinements} />
}
Have we decoupled our
business logic from
the churn?
Hackathon #2 🚀
React Native
3 developers, 2 days, no iOS experience
import { refine } from 'seek-refine-job-search';
const refinements = refine(this.props.params);
render() {
return (
<View style={styles.salaryRange}>
<Text>Min</Text>
<PickerIOS>
{
refinements.salary.from
.filter(salary => salary.isActive)
.map(({ value, label }) => (
<PickerIOS.Item value={value} label={label} />
))
}
</PickerIOS>
</View>
);
}
Given their importance,
they lacked love 💔
Inconsistently replicated,
highly coupled code
[
'/jobs/:worktype',
'/jobs/in-:location',
'/jobs/in-:location/in-:suburb',
'/jobs/in-:location/in-:suburb/:worktype',
'/jobs/in-:location/:worktype',
'/jobs/in-:location/:area',
'/jobs/in-:location/:area/:worktype',
'/jobs-in-:classification',
'/jobs-in-:classification/:worktype',
'/jobs-in-:classification/in-:location',
'/jobs-in-:classification/in-:location/in-:suburb',
'/jobs-in-:classification/in-:location/in-:suburb/:worktype',
'/jobs-in-:classification/in-:location/:worktype',
'/jobs-in-:classification/in-:location/:area',
'/jobs-in-:classification/in-:location/:area/:worktype',
'/jobs-in-:classification/:subclassification',
'/jobs-in-:classification/:subclassification/in-:location',
'/jobs-in-:classification/:subclassification/in-:location/in-:suburb',
'/jobs-in-:classification/:subclassification/in-:location/in-:suburb/:worktype',
'/jobs-in-:classification/:subclassification/in-:location/:worktype',
'/jobs-in-:classification/:subclassification/in-:location/:area',
'/jobs-in-:classification/:subclassification/in-:location/:area/:worktype',
'/jobs-in-:classification/:subclassification/:worktype'
].forEach(function(friendlyUrl) {
$stateProvider.state(friendlyUrl.replace(/[\/\:\{\|\}]/g, '_'), {
url: friendlyUrl,
resolve: {
redirect: function () {
var goToResults = function (searchQueryString) {
$location.replace().path('/jobsearch').search(searchQueryString);
},
goToHome = function () {
$location.replace().path('/');
};
return searchStateParamsConverter
.getQueryStringFromFriendlyUrlStateParams($stateParams)
.then(goToResults, goToHome);
}]
}
});
});
<if url="^/information-communication-technology-jobs(.*)$">
<redirect url="^/information-communication-technology-jobs/all(.*)?$" to="/jobs-in-information-communication-technology$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/in-(.*)/(.*)?$" to="/jobs-in-information-communication-technology/in-$1/$2$3" permanent="true" />
<redirect url="^/information-communication-technology-jobs/(full-time|part-time|contract|casual)(.*)?$" to="/jobs-in-information-communication-technology/$1$2" permanent="true" />
<redirect url="^/information-communication-technology-jobs/architects(.*)?$" to="/jobs-in-information-communication-technology/architects$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/business-systems-analysts(.*)?$" to="/jobs-in-information-communication-technology/business-systems-analysts$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/computer-operators(.*)?$" to="/jobs-in-information-communication-technology/computer-operators$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/consultants(.*)?$" to="/jobs-in-information-communication-technology/consultants$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/database-development-administration(.*)?$" to="/jobs-in-information-communication-technology/database-development-administration$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/developers-programmers(.*)?$" to="/jobs-in-information-communication-technology/developers-programmers$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/engineering-hardware(.*)?$" to="/jobs-in-information-communication-technology/engineering-hardware$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/engineering-network(.*)?$" to="/jobs-in-information-communication-technology/engineering-network$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/engineering-software(.*)?$" to="/jobs-in-information-communication-technology/engineering-software$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/help-desk-it-support(.*)?$" to="/jobs-in-information-communication-technology/help-desk-it-support$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/management(.*)?$" to="/jobs-in-information-communication-technology/management$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/networks-systems-administration(.*)?$" to="/jobs-in-information-communication-technology/networks-systems-administration$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/product-management-development(.*)?$" to="/jobs-in-information-communication-technology/product-management-development$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/programme-project-management(.*)?$" to="/jobs-in-information-communication-technology/programme-project-management$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/sales-pre-post(.*)?$" to="/jobs-in-information-communication-technology/sales-pre-post$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/security(.*)?$" to="/jobs-in-information-communication-technology/security$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/team-leaders(.*)?$" to="/jobs-in-information-communication-technology/team-leaders$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/technical-writing(.*)?$" to="/jobs-in-information-communication-technology/technical-writing$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/telecommunications(.*)?$" to="/jobs-in-information-communication-technology/telecommunications$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/testing-quality-assurance(.*)?$" to="/jobs-in-information-communication-technology/testing-quality-assurance$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/web-development-production(.*)?$" to="/jobs-in-information-communication-technology/web-development-production$1" permanent="true" />
<redirect url="^/information-communication-technology-jobs/other(.*)?$" to="/jobs-in-information-communication-technology/other$1" permanent="true" />
</if>
Over 700 lines of static,
untested XML config… 😩
import { routes } from 'seek-jobs-seo';
export default (
<Route component={App}>
<Route path="/" component={Home} />
{
routes.map(path =>
<Route path={path} component={SearchResults} />)
}
</Route>
);
Router has no knowledge
of friendly urls
Handling links inside the app
Search Params 👉 Friendly Url
eg: { classification: 1200 } 👉 '/jobs-in-accounting'
Friendly Url 👉 Search Params
eg: '/jobs-in-accounting' 👉 { classification: 1200 }
{
should: 'qualify valid graduatesearch with work type',
input: {
path: '/jobs',
query: {
graduatesearch: 'true',
worktype: '242'
}
},
output: { path: '/jobs/graduate/full-time', query: {} }
}
import { qualifyUrl } from 'seek-jobs-seo';
render() {
const { path, query } = qualifyUrl(locale, path, query);
return (
<Link to={`${path}?${query}`}>
{ children }
</Link>
);
}
Friendly URLs,
Qualifying,
Meta Descriptions,
Canonicals
All vanilla 🍦
Framework agnostic 🙈
Protected from the churn 👮
How can we reduce friction
to separating modules? 😬
Module boundaries are
not always obvious 🕵
How can we stage modules
while proving out the api
Webpack
Resolving module directories
// Webpack config
module.exports = {
…
resolve: {
modulesDirectories: [ 'node_modules', 'wip_modules' ]
}
…
};
/app
/components
/node_modules
/wip_modules
/seek-jobs-api-client
Using staging modules:
import { search } from 'seek-jobs-api-client';
Extract 🛫
Test ✅
Document 📜
Install 🛬
Using `node_modules`:
import { search } from 'seek-jobs-api-client';
If unit testing happens outside,
what happens inside? 🤔
Find confidence without
testing yourself into a corner.
The application is an integration module
Want to avoid re-coupling with tests 🔗
We’ve seen this pay off already 🤑
Needed to introduce a
state management library
Enter Flummox…
npm install flummox --save
Universal 🌏
Light weight 🍃
Clean API 👌
Churn had struck.
Fatigue was setting in. 😴
Switched to Redux
npm install react-redux --save
Tests provide confidence
to refactor, not hinder it 💪
Suffered just like our business logic 😔
Rebuilding the same guidelines
again and again and again
Each time coupled to a framework 🔗
Standardised on preprocessor — LESS
Can we share design guidelines
if not components? 🛤
// Webpack config
module.exports = {
…
resolve: {
modulesDirectories: [ 'node_modules', 'wip_modules' ]
}
…
};
Leverage staging modules
/wip_modules
/theme
/grid
/palette
/type
/theme.less
theme.less
@import (reference) 'grid/grid.less'
@import (reference) 'palette/palette.less'
@import (reference) 'type/type.less'
palette.less
@sk-blue: #0d3880;
@sk-pink: #e60278;
@sk-green: #178a00;
type.less
@base-font-stack: "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif;
@base-font-size: 10;
@font-descender-height-scale: 0.12;
@hero-type-row-span: 5;
@hero-type-scale: 4.2;
@headline-type-row-span: 4;
@headline-type-scale: 2.8;
@heading-type-row-span: 3;
@heading-type-scale: 2.1;
@subheading-type-row-span: 3;
@subheading-type-scale: 1.8;
@standard-type-row-span: 2;
@standard-type-scale: 1.4;
No resulting CSS, just guidelines 📐
@import (reference) '~theme'
.button {
.touchableText();
background-color: @sk-pink;
color: @sk-white;
cursor: pointer;
}
import styles from './button.less';
render() {
return (
<button className={styles.button}>
{ children }
</button>
);
}
CSSModules gives us style encapsulation 💊
CSSModules protect us
from framework churn ⚔
Same styles could be imported
to an Angular component 💣
CSSModules gives our design guidelines
what vanilla gives our business logic 💂
What if we could share the components… 🤔
Hackathon #3 🚀
Living Style Guide 🎨
Core design principles 📐🖍
Suite of React components
that implement the guidelines
import { Button, StarIcon } from 'seek-style-guide/react';
render() {
<Button>
<StarIcon />
Save
</Button>
}
Huge development speed up 🏎
Makes consistent visual style easier
Style guide is now powering 5 products 🔌
Canonical list of core
components and guidelines
Switching from React is
essentially swapping templates
Micro library mentality
focusing of pure functions 😇
“Best way to reduce complexity
in your app is to take it away”
Choose frameworks that
solve actual problems
Beware of blogs
Engineer for change, it’s inevitable
@michaeltaranto
github.com/mjt01
bit.ly/javascript-fatigue