strategies for mitigating complexity in react based redux applicaitons

Post on 03-Mar-2017

103 Views

Category:

Engineering

4 Downloads

Preview:

Click to see full reader

TRANSCRIPT

HELLO,REACT.JS VANCOUVER!

Megabe.scholz@unbounce.com

https://github.com/garbles

Strategies for Mitigating Complexity in React

Based Redux Applications

Designing for Simplicity

"The computing scientist’s main challenge is not to get confused by the complexities of his own making."

- E. W. Dijkstra

"The bottom line is that simplicity is a choice. It's your fault if you don't have a simple system."

- Rich Hickey (Simple Made Easy)

Tests

"Complexity is the root cause of the vast majority of problems with software today… The primary status of complexity as the major cause comes simply from the fact that being able to understand a system is a prerequisite for avoiding all of them, and of course it is this which complexity destroys."

- Moseley & Marks

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

Stateless components

"The core premise for React is that UIs are simply a projection of data into a different form of data. The same input gives the same output."

- Sebastian Markbåge

class Dropdown extends React.Component { constructor() { super();

this.state = {open: false}; this.handleToggle = () => { this.props.onToggle(); this.setState({open: !this.state.open}); } }

render() { const { options, selected, onSelect } = this.props;

const { open } = this.state;

// render some stuff }}

class Dropdown extends React.Component { constructor() { super();

this.state = {open: false}; this.handleToggle = () => { this.props.onToggle(); this.setState({open: !this.state.open}); } }

render() { const { options, selected, onSelect } = this.props;

const { open } = this.state;

// render some stuff }}

<Dropdown options={optionsA} selected={selectedA} onToggle={someFn} onSelect={someOtherFn}/>

<Dropdown options={optionsB} selected={selectedB} onToggle={someFn} onSelect={someOtherFn}/>

<Dropdown options={optionsA} selected={selectedA} onToggle={someFn} onSelect={someOtherFn}+ open={open}/>

<Dropdown options={optionsB} selected={selectedB} onToggle={someFn} onSelect={someOtherFn}+ open={!open}/>

class Dropdown extends React.Component {- constructor() {- super();-- this.state = {open: false};- this.handleToggle =- () => {- this.props.onToggle();- this.setState({open: !this.state.open});- }- }- render() { const { options, selected, onSelect,+ open,+ onToggle } = this.props;

- const {- open- } = this.state;

// render some stuff }}

class StatefulDropdown extends React.Component { constructor() { super(); this.state = {open: false};

this.handleSelect = () => this.setState({open: !this.state.open}); }

render() { return ( <Dropdown options={this.props.options} selected={this.props.selected} open={this.state.open} onToggle={this.handleToggle} onSelect={this.props.onSelect} /> ); }}

class StatefulDropdown extends React.Component { constructor() { super(); this.state = {open: false};

this.handleSelect = () => this.setState({open: !this.state.open}); }

render() { return ( <Dropdown options={this.props.options} selected={this.props.selected} open={this.state.open} onToggle={this.handleToggle} onSelect={this.props.onSelect} /> ); }}

Always keep lowest level components stateless

Keep them ALL stateless if you can

Switches can be flipped in a store and it Just Works™. More control.

You can always wrap a stateless component if you need stateful behavior.

Use Redux containers liberally

function Targeting(props) { const { advancedTargetingAvailable, advancedTargetingEnabled, advancedTargetingExpanded, cookieTargetingEnabled, domain, hideRules, urlTargets, onRuleChangeAndSave, onRuleChange, onRemoveRule, onAddRule, onDomainChange, frequency, cookieTarget, geoTargets, referrerTargets, onChangeFrequency, onBlurFrequency, onChangeCookieType, onBlurCookieName, onChangeCookieName, onExpandAdvancedTargeting, onToggleGeoTargetsEnabled, onToggleReferrerTargetsEnabled, onChangeGeoTargets, planUpgradeLink, firstName,

function Targeting(props) { const { domain, advancedTargetingAvailable } = props;

return ( <div> <div> Domain: {domain} Pro Account? {advancedTargetingAvailable} </div> <FrequencyContainer /> <GeoContainer /> <ReferrerContainer /> </div> );}

<Provider store={store}> <TargetingContainer /></Provider>

<Provider store={store}> <FrequencyContainer /></Provider>

Low cost for huge readability win (components only require what they need).

Depending on hierarchy it may be a perf win.

Normalize at boundaries.

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

const initialState = { backgroundColor: 'white'};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; default: return state; }};

const AppContainer = connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => dispatch({type: 'CHANGE_COLOR', color}) }))(App);

const initialState = { backgroundColor: 'white'};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; default: return state; }};

const AppContainer = connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => dispatch({type: 'CHANGE_COLOR', color}) }))(App);

const initialState = { backgroundColor: 'white'};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; default: return state; }};

const AppContainer = connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => dispatch({type: 'CHANGE_COLOR', color}) }))(App);

Requirements change!

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }

const state = { backgroundColor: 'white'

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

const initialState = { backgroundColor: 'white',+ borderColor: null};

class App extends React.Component { render() { const { backgroundColor, onClick,+ borderColor } = this.props;

+ const border = borderColor ? `35px solid ${borderColor}` : null;+ return (- <div style={{backgroundColor}}>+ <div style={{backgroundColor, border}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

const initialState = { backgroundColor: 'white',+ borderColor: null};

class App extends React.Component { render() { const { backgroundColor, onClick,+ borderColor } = this.props;

+ const border = borderColor ? `35px solid ${borderColor}` : null;+ return (- <div style={{backgroundColor}}>+ <div style={{backgroundColor, border}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color,+ borderColor: action.color === 'red' ? 'purple' : null }; default: return state; }};

Requirements change!

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

Red

Green

Blue

class App extends React.Component { render() { const { backgroundColor, onClick } = this.props;

return ( <div style={{backgroundColor}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }

(state, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color }; }};

const state = { backgroundColor: 'white'};

connect( // mapStateToProps state => state,

// mapDispatchToProps dispatch => ({ onClick: color => {type: 'CHANGE_COLOR', color} }))(App);

const initialState = { backgroundColor: 'white', borderColor: null,+ width: '100%'};

class App extends React.Component { render() { const { backgroundColor, onClick, borderColor,+ width } = this.props;

const border = borderColor ? `35px solid ${borderColor}` : null;

return (- <div style={{backgroundColor, border}}>+ <div style={{backgroundColor, border, width}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

const initialState = { backgroundColor: 'white', borderColor: null,+ width: '100%'};

class App extends React.Component { render() { const { backgroundColor, onClick, borderColor,+ width } = this.props;

const border = borderColor ? `35px solid ${borderColor}` : null;

return (- <div style={{backgroundColor, border}}>+ <div style={{backgroundColor, border, width}}> <button onClick={() => onClick('red')}> Red </button> <button onClick={() => onClick('green')}> Green </button> <button onClick={() => onClick('red')}> Blue </button> </div> ) }}

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color, borderColor: action.color === 'red' ? 'purple' : null,+ width: action.color === 'green' ? '50%' : '100%' }; default: return state; }};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color, borderColor: action.color === 'red' ? 'purple' : null,+ borderRadius: action.color === 'blue' ? '50%' : 0, width: action.color === 'green' ? '50%' : '100%', }; default: return state; }};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color,- borderColor: action.color === 'red' ? 'purple' : null,+ borderColor: action.color === 'red' || action.color === 'yellow' ? 'purple' : null,- borderRadius: action.color === 'blue' ? '50%' : 0,+ borderRadius: action.color === 'blue' || action.color === 'yellow' ? '50%' : 0,- width: action.color === 'green' ? '50%' : '100%',+ width: action.color === 'green' || action.color === 'yellow' ? '50%' : '100%', }; default: return state; }};

function reducer(state, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, backgroundColor: action.color, borderColor: action.color === 'red' || action.color === 'yellow' ? 'purple' : null, borderRadius: action.color === 'blue' || action.color === 'yellow' ? '50%' : 0, width: action.color === 'green' || action.color === 'yellow' ? '50%' : '100%', }; default: return state; }};

function reducer(state, action) { switch (action.type) { case 'CHOOSE_RED': return { ...state, backgroundColor: 'red', borderColor: 'purple', borderRadius: 0, width: '100%' }; case 'CHOOSE_BLUE': return { ...state, backgroundColor: 'blue', borderColor: null, borderRadius: '50%', width: '100%' }; case 'CHOOSE_GREEN': return { ...state, backgroundColor: 'green', borderColor: null, borderRadius: 0, width: '50%' }; case 'CHOOSE_YELLOW': return { ...state, backgroundColor: 'yellow', borderColor: 'purple', borderRadius: '50%', width: '50%' }; default: return state; }};

class App extends React.Component { render() { const { backgroundColor, onClickRed, onClickGreen, onClickBlue, onClickYellow borderColor, width } = this.props;

const border = borderColor ? `35px solid ${borderColor}` : null;

return ( <div style={{backgroundColor, border, width}}> <button onClick={() => onClickRed()}> Red </button> <button onClick={() => onClickGreen()}> Green </button> <button onClick={() => onClickBlue()}> Blue </button> <button onClick={() => onClickYellow()}> Yellow </button> </div> ) }}

class App extends React.Component { render() { const { backgroundColor, onClickRed, onClickGreen, onClickBlue, onClickYellow borderColor, width } = this.props;

const border = borderColor ? `35px solid ${borderColor}` : null;

return ( <div style={{backgroundColor, border, width}}> <button onClick={() => onClickRed()}> Red </button> <button onClick={() => onClickGreen()}> Green </button> <button onClick={() => onClickBlue()}> Blue </button> <button onClick={() => onClickYellow()}> Yellow </button> </div> ) }}

Boilerplate (more actions, larger reducers).

Code is easier to remove if it is no longer required.

Fewer code paths.

Easier to type check if you're into that.

Side-effects at one point in the update loop.

Action Dispatcher Store View

Action

Async

Action Dispatcher Store

View

Action

Async

Action Dispatcher Store

View

Action

?

const initialState = { user: { id: 1, name: 'Bob' }, busyState: 'READY'};

function reducer(state, action) { switch (action.type) { case 'SAVING_USER': return { ...state, busyState: 'SAVING' }; case 'SAVING_USER_COMPLETE': return { ...state, busyState: 'READY' }; default: return state; }}

let watch = watcher();

watch = applyHandler( watch, ['busyState', 'user.name'], ([busyState, name]) => busyState === 'SAVING' && name.length > 0 updateUser, store => store.dispatch(savingUserComplete()));

const store = createStore(reducer, initialState, watch);

const initialState = { user: { id: 1, name: 'Bob' }, busyState: 'READY'};

function reducer(state, action) { switch (action.type) { case 'SAVING_USER': return { ...state, busyState: 'SAVING' }; case 'SAVING_USER_COMPLETE': return { ...state, busyState: 'READY' }; default: return state; }}

let watch = watcher();

watch = applyHandler( watch, ['busyState', 'user.name'], ([busyState, name]) => busyState === 'SAVING' && name.length > 0 updateUser, store => store.dispatch(savingUserComplete()));

const store = createStore(reducer, initialState, watch);

const initialState = { user: { id: 1, name: 'Bob' }, busyState: 'READY'};

function reducer(state, action) { switch (action.type) { case 'SAVING_USER': return { ...state, busyState: 'SAVING' }; case 'SAVING_USER_COMPLETE': return { ...state, busyState: 'READY' }; default: return state; }}

let watch = watcher();

watch = applyHandler( watch, ['busyState', 'user.name'], ([busyState, name]) => busyState === 'SAVING' && name.length > 0 updateUser, store => store.dispatch(savingUserComplete()));

const store = createStore(reducer, initialState, watch);

const initialState = { user: { id: 1, name: 'Bob' }, busyState: 'READY'};

function reducer(state, action) { switch (action.type) { case 'SAVING_USER': return { ...state, busyState: 'SAVING' }; case 'SAVING_USER_COMPLETE': return { ...state, busyState: 'READY' }; default: return state; }}

let watch = watcher();

watch = applyHandler( watch, ['busyState', 'user.name'], ([busyState, name]) => busyState === 'SAVING' && name.length > 0 updateUser, store => store.dispatch(savingUserComplete()));

const store = createStore(reducer, initialState, watch);

All actions are synchronous.

Async is represented in state by default.

All side-effects occur at the end of the update loop.

Easier to substitute based on the context.

COMPLEX SIMPLE

Stateful components. Stateless components.

Too many props. Use Redux containers liberally.

Conditional statements in reducers. Normalize at boundaries.

Scattered with side-effects. Side-effects at one point in the update loop.

THANKS!

https://www.infoq.com/presentations/Simple-Made-Easy

http://shaffner.us/cs/papers/tarpit.pdf

https://github.com/reactjs/react-basic

top related