😴
Fighting Javascript Fatigue

“Extreme tiredness caused by library churn in Github’s most active community”

Fatigue strikes down many… 💤

Developers 🤓

Companies 🏢

Customers 🛍

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? 🤕

Business rules

SEO (routing)

Consistent visual style

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

🙅🏆🏻

...but we did it 🎉

Decoupling SEO 🐛

URLs are king 👑

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… 😩

Seo-As-A-Service 🍽

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 in one place 👌

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

Started open sourcing 😍

If unit testing happens outside,
what happens inside? 🤔

Find confidence without
testing yourself into a corner.

Webdriver tests 😱

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 👌

28 days later… 🗓

Churn had struck.
Fatigue was setting in. 😴

Switched to Redux

npm install react-redux --save

Tests provide confidence
to refactor, not hinder it 💪

Visual Styling 🎨

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

Learnings 📚

Keep your core vanilla 🍦

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

Please no more 👶🛁💦

@michaeltaranto

github.com/mjt01

bit.ly/javascript-fatigue