I write
Work parallelisation
Controlling shared abstractions
Refactoring
AB Tests
Integration Tests
A set of principles that helps you
to define the boundaries.
Code will be
maintained over a longer
period of time and
Β size will increase?
for you
not for you
yes
no
Co-location
Decoupling
Disposability
Goals
src/
βββ components/
| βββ Login.js
βββ containers/
| βββ Login.js
βββ actionCreators/
| βββ login.js
βββ reducers/
βββ login.js
src/
βββ features/
βββ featureA/
βββ featureB/
βββ featureC/
Organise by featuresΒ instead of types.
Feature is a self-contained user facing reusable
complex building block.
Application
Pages / Screens
Features
Components
Utilities
src/
βββ features/
βββ featureA/
| βββ privateModuleA.js
| βββ privateModuleB.js
| βββ privateModuleC.js
| βββ index.js
|
βββ featureB/
βββ featureC/
src/
βββ shared/
| βββ components/
| βββ api/
βββ features/
βββ featureA/
βββ featureB/
βββ featureC/
Changing shared code
with 100% unit test coverage
can still break something.
const f = (value) => {
if (typeof value === 'object') {
return {...value, say: 'Hello'}
}
return value
}
// How we think it should be used.
f({say: 'hi'}) // {say: 'Hello'}
// Reality
f(null) // {say: 'Hi'}
const f = (value) => {
if (
typeof value === 'object' &&
value.say === 'hi'
) {
return {...value, say: 'Hello'}
}
return value
}
// How we think it should be used.
f({say: 'hi'}) // {say: 'Hello'}
// Reality
f(null) // Uncaught TypeError
const f = (fn) => {
... some code
fn()
... some code
return 1
}
Use pure functional utilities.
Writte moreΒ tests.
Carefully design the interface.
Do more detailed code reviews.
class JustAnotherCounter extends Component {
state = {
count: 0
};
setCount = () => {
this.setState({
count: this.state.count + 1
});
};
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.setCount}>
Count Up To The Moon
</button>
</div>
);
}
}
import React, { useState } from 'react';
function JustAnotherCounter() {
const [count, setCount] = useState(0);
const onClick = () => setCount(count + 1);
return (
<div>
<h1>{count}</h1>
<button
onClick={onClick}
>
Count Up To The Moon
</button>
</div>
);
}
Code structure
src/
βββ shared/
| βββ components/
| βββ api/
βββ features/
βββ {feature}/
βββ containers/
βββ actionCreators/
βββ actionTypes/
βββ renderers/
βββ selectors/
βββ reducer
Directory structure
Never Break Those Rules.
import {Header} from 'features/header';
import {LoginForm} from 'features/login';
const PageA = () => (
<Fragment>
<Header />
<LoginForm />
</Fragment>
);
import {Header} from 'features/header';
import {LoginForm} from 'features/login';
const PageA = () => {
const [loginStatus, setLoginStatus] = useState('loggedOut');
const onLogin = () => setLoginStatus('loggedIn');
const onLogout = () => setLoginStatus('loggedOut');
return (
<Fragment>
<Header loginStatus={loginStatus} onLogin={onLogin} ... />
<LoginForm onLogin={onLogin} />
</Fragment>
);
};
Lets add login button to the header.
const Header = ({loginStatus, onLogin, onLogout}) => {
return (
<Fragment>
<A />
<B />
{loginStatus === 'loggedOut' &&
<button onClick={onLogin}>Login<button>}
{loginStatus === 'loggedIn' &&
<button onClick={onLogout}>Logout<button>}
</Fragment>
);
};
What are the problems here?
Coupled
const Header = ({loginLogoutButton}) => {
return (
<Fragment>
<A />
<B />
{loginLogoutButton}
</Fragment>
);
};
import {Header} from 'features/header';
import {LoginForm, LoginLogoutButton} from 'features/login';
const PageA = () => (
<Fragment>
<Header loginLogoutButton={<LoginLogoutButton />} />
<LoginForm />
</Fragment>
);
How does LoginLogoutButton know what to do?
const LoginContext = React.createContext('login');
const LoginProvider = ({children}) => {
const [status, setLoginStatus] = useState('loggedOut');
const onLogin = data => api.login(data).then(setLoginStatus);
const onLogout = data => api.logout(data).then(setLoginStatus);
return (
<LoginContext.Provider value={{status, onLogin, onLogout}}>
{children}
</LoginContext.Provider>
);
};
const LoginLogoutButton = () => (
<LoginContext.Consumer>
{({status, onLogin, onLogout}) =>
status === 'loggedIn' ? (
<button onClick={onLogout}>logout</button>
) : (
<button onClick={onLogin}>login</button>
)
}
</LoginContext.Consumer>
);
import {Header} from 'features/header';
import {
LoginForm,
LoginLogoutButton,
LoginProvider
} from 'features/login';
const PageA = () => (
<LoginProvider>
<Header loginLogoutButton={<LoginLogoutButton />} />
<LoginForm />
</LoginProvider>
);
π
Β
Do Not optimize for Modification!
Unless you can
predict the future.
Optimize for ease of removal!
Based on what you
already know.
At some point everything is going to be a Mess.
Refactor everything
vs.
Refactor isolated parts.
Co-location
Isolation
Disposability
Work parallelisation
Controlling shared abstractions
Discoverability
Integration Tests
AB Tests
Refactoring
π