Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Smells in React Apps

Smells in React Apps

Presented at JSConf.Asia 2018

Avatar for Thai Pangsakulyanont

Thai Pangsakulyanont

January 27, 2018
Tweet

More Decks by Thai Pangsakulyanont

Other Decks in Technology

Transcript

  1. Slide № 5 Refactoring: Improving the Design of Existing Code

    Clean Code: A Handbook of Agile Software Craftsmanship Agile Software Development, Principles, Patterns, and Practices
  2. Slide № 25 Object-oriented analysis and design Clean architecture Functional

    programming Automated testing Continuous integration Linting Type systems Modularization Componentization Code smells …
  3. Slide № 54 Object-oriented analysis and design Clean architecture Functional

    programming Automated testing Continuous integration Linting Type systems Modularization Componentization Code smells …
  4. Slide № 55 Object-oriented analysis and design Clean architecture Functional

    programming Automated testing Continuous integration Linting Type systems Modularization Componentization Code smells … Tech stuffs
  5. Slide № 58 // MusicPreviewer.js export function preload () {

    getInstance() } export function enable () { return getInstance().enable() } export function disable () { return getInstance().disable() } export function go () { return getInstance().go() } export function preview (url) { return getInstance().preview(url) } MusicPreviewer
  6. Slide № 59 // MusicPreviewer.js export function preload () {

    getInstance() } export function enable () { return getInstance().enable() } export function disable () { return getInstance().disable() } export function go () { return getInstance().go() } export function preview (url) { return getInstance().preview(url) } MusicPreviewer exports 5 functions
  7. Slide № 60 // MusicPreviewer.js export function preload () {

    getInstance() } export function enable () { return getInstance().enable() } export function disable () { return getInstance().disable() } export function go () { return getInstance().go() } export function preview (url) { return getInstance().preview(url) } import * as MusicPreviewer from MusicPreviewer.preload() MusicPreviewer.enable() MusicPreviewer.disable() MusicPreviewer.go() MusicPreviewer.preview(url) User code:
  8. Slide № 62 let instance const getInstance = () =>

    instance || ( instance = createMusicPreviewer() ) function createMusicPreviewer () { let enabled = false let currentUrl = null let backgroundLoaded = false let backgroundPlayed = false const instances = {} function update () { if (!enabled) { if (backgroundPlayed) { backgroundFader.fadeTo(0, 100) backgroundPlayed = false background.pause() } for (const key of Object.keys(instances)) { const instance = instances[key] instance.destroy() } return } let playing = null for (const key of Object.keys(instances)) { const instance = instances[key] if (key === currentUrl) { if (instance.loaded) { instance.play() playing = instance } else { instance.stop() } } if (playing) { backgroundFader.fadeTo(0, 1) } else { backgroundFader.fadeTo(0.4, 0.5) if (backgroundLoaded && !backgroundPlayed) { backgroundPlayed = true try { background.play() } catch (e) { console.warn('Cannot play background music') } } } } const musicPreviewer = { enable () { if (enabled) return enabled = true update() }, disable () { if (!enabled) return enabled = false update() }, go () { if (!enabled) return goSound.currentTime = 0
  9. Slide № 63 Wrote a hack behind a well-designed interface

    In principle: Bad code stays behind that interface.
  10. Slide № 64 Wrote a hack behind a well-designed interface

    In principle: Bad code stays behind that interface. In practice: Another person looks for a technical solution,
  11. Slide № 65 Wrote a hack behind a well-designed interface

    In principle: Bad code stays behind that interface. In practice: Another person looks for a technical solution, found my hack (but didn’t know it was a hack),
  12. Slide № 66 Wrote a hack behind a well-designed interface

    In principle: Bad code stays behind that interface. In practice: Another person looks for a technical solution, found my hack (but didn’t know it was a hack), and duplicated the code!
  13. Slide № 75 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component
  14. Slide № 76 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component
  15. Slide № 78 Thai Pangsakulyanont 1 minute ago Let’s go

    to the Marina Bay Carnival after the conference!!!!
  16. Slide № 80 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component
  17. Slide № 81 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component
  18. Slide № 82 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component
  19. Slide № 83 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component <UserAvatar size='message' … />
  20. Slide № 92 UserAvatar uses specific feature generic component I

    know how big to display in a message Message
  21. Slide № 93 UserAvatar uses aware of specific feature generic

    component Message I know how big to display in a message
  22. Slide № 95 UserAvatar Message uses aware of specific feature

    generic component Information leaking. Knows how to render itself Except: Doesn’t know the appropriate size of the avatar
  23. Slide № 96 UserAvatar Message uses aware of specific feature

    generic component Information leaking. Knows how to render itself Except: Doesn’t know the appropriate size of the avatar The knowledge of the size, which should have belonged to the messaging system is here
  24. Slide № 100 <UserAvatar size='small' … /> <UserAvatar size='medium' …

    /> <UserAvatar size='large' … /> A UserAvatar component <UserAvatar size='message' … />
  25. Slide № 102 UserAvatar specific feature generic component I need

    to show an avatar! It’s going to have size M! Message
  26. Slide № 103 A UserAvatar component function UserAvatar (props) {

    … } UserAvatar.propTypes = { size: PropTypes.oneOf([ 'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XX … }
  27. Slide № 104 A UserAvatar component function UserAvatar (props) {

    … } UserAvatar.propTypes = { size: PropTypes.oneOf([ 'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XX … }
  28. Slide № 105 A UserAvatar component function UserAvatar (props) {

    … } UserAvatar.propTypes = { size: PropTypes.number, … }
  29. Slide № 106 A UserAvatar component function UserAvatar (props) {

    … } UserAvatar.propTypes = { size: PropTypes.oneOf([ 18, 24, 30, 36, 48, … ]), … }
  30. Slide № 114 Upload file Select file on computer… Dropbox

    Google Drive Emergency alert Select file on computer…
  31. Slide № 115 Upload file Select file on computer… Dropbox

    Google Drive Emergency alert <Button>
  32. Slide № 116 Upload file Select file on computer… Dropbox

    Google Drive Emergency alert <Menu> <Button>
  33. Slide № 118 Select file on computer… Dropbox Google Drive

    Emergency alert <MenuButton> Upload file
  34. Slide № 121 Select file on computer… Dropbox Google Drive

    Emergency alert <MenuButton> Upload file
  35. Slide № 122 Select file on computer… Dropbox Google Drive

    Emergency alert <MenuButton> Upload file Select file on computer…
  36. Slide № 123 Select file on computer… Dropbox Google Drive

    Emergency alert <MenuButton> Upload file Select file on computer… Button must appear to be pressed while menu is open
  37. Slide № 139 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> </Navbar> ) }
  38. Slide № 140 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> </Navbar> ) }
  39. Slide № 141 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> </Navbar> ) }
  40. Slide № 146 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> </Navbar> ) } }
  41. Slide № 147 class Navigation extends Component { 
 render

    () { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav>
  42. Slide № 148 class Navigation extends Component {
 static propTypes

    = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav>
  43. Slide № 149 class Navigation extends Component { static propTypes

    = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav>
  44. Slide № 150 const loggedIn = !!this.props.currentUser return ( <Navbar>

    <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> </Navbar> ) } }
  45. Slide № 151 const loggedIn = !!this.props.currentUser return ( <Navbar>

    <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> <Nav pullRight> </Nav> </Navbar> ) } }
  46. Slide № 152 const loggedIn = !!this.props.currentUser return ( <Navbar>

    <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> <Nav pullRight> {!loggedIn && <NavItem href='#' onClick={this.props.onLogin}>Login</NavI {loggedIn && <NavItem href='#' onClick={this.props.onLogout}>Logout</Nav </Nav> </Navbar> ) } }
  47. Slide № 153 class Navigation extends Component { static propTypes

    = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header>
  48. Slide № 154 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header>
  49. Slide № 155 const enhance = connect( state => ({

    currentUser: selectCurrentUser(props) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header>
  50. Slide № 156 const enhance = connect( state => ({

    currentUser: selectCurrentUser(props) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> Selecting data from store
  51. Slide № 157 const enhance = connect( state => ({

    currentUser: selectCurrentUser(props) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> Selecting data from store Dispatching action to store
  52. Slide № 158 const enhance = connect( state => ({

    currentUser: selectCurrentUser(props) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> Selecting data from store Dispatching action to store Rendering logic
  53. Slide № 163 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header>
  54. Slide № 164 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> Selecting data from store Dispatching action to store Rendering logic
  55. Slide № 165 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> Selecting data from store Dispatching action to store Rendering logic Go with the flow!
  56. Slide № 166 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header>
  57. Slide № 167 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser
  58. Slide № 168 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser
  59. Slide № 169 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser
  60. Slide № 170 onViewMessages: MessagesIO.viewMessages } ) class Navigation extends

    Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem>
  61. Slide № 171 onViewMessages: MessagesIO.viewMessages } ) class Navigation extends

    Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header>
  62. Slide № 172 onViewMessages: MessagesIO.viewMessages } ) class Navigation extends

    Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header>
  63. Slide № 173 <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand>

    </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> <Nav pullRight> {!loggedIn && <NavItem href='#' onClick={this.props.onLogin}>Login</NavI {loggedIn && <NavItem href='#' onClick={this.props.onLogout}>Logout</Nav </Nav> </Navbar> ) } }
  64. Slide № 174 <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand>

    </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> </Nav> <Nav pullRight>
  65. Slide № 175 <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand>

    </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) </NavItem> } </Nav> <Nav pullRight>
  66. Slide № 176 <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand>

    </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) </NavItem> } {loggedIn && <NavItem href='/inbox' onClick={this.props.onViewMessages}> Messages ({this.props.newMessageCount}) </NavItem> } </Nav> <Nav pullRight>
  67. Slide № 177 <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand>

    </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) </NavItem> } {loggedIn && <NavItem href='/inbox' onClick={this.props.onViewMessages}> Messages ({this.props.newMessageCount}) </NavItem> } </Nav> <Nav pullRight>
  68. Slide № 178 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount})
  69. Slide № 179 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) Selecting data from store Dispatching action to store Rendering logic
  70. Slide № 180 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) Selecting data from store Dispatching action to store Rendering logic Cross-cutting concerns
  71. Slide № 181 User Notification Message User Message User Notification

    Message User Navigation Navigation User Navigation const enhance = connect( state => ({ currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount})
  72. Slide № 182 User Notification Message User Message User Notification

    Message User Navigation Navigation User Navigation const enhance = connect( state => ({ currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount})
  73. Slide № 186 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  74. Slide № 187 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) + ), + onToggleGauge: ({ options }) => () => ( + OptionsIO.setOptions({ + 'player.P1.gauge': Options.toggleGauge(options['player.P1.gauge']) + }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  75. Slide № 188 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) + ), + onToggleGauge: ({ options }) => () => ( + OptionsIO.setOptions({ + 'player.P1.gauge': Options.toggleGauge(options['player.P1.gauge']) + }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, + onToggleGauge: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  76. Slide № 189 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) + ), + onToggleGauge: ({ options }) => () => ( + OptionsIO.setOptions({ + 'player.P1.gauge': Options.toggleGauge(options['player.P1.gauge']) + }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, + onToggleGauge: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  77. Slide № 190 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) + ), + onToggleGauge: ({ options }) => () => ( + OptionsIO.setOptions({ + 'player.P1.gauge': Options.toggleGauge(options['player.P1.gauge']) + }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, + onToggleGauge: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> + <OptionsPlayer.Row label="Gauge"> + <OptionsCheckbox + checked={Options.isGaugeEnabled(this.props.options)} + onToggle={this.props.onToggleGauge} + > + Show expert gauge <span className="OptionsPlayerͷhint">(experimental)</span> + </OptionsCheckbox> + </OptionsPlayer.Row> + <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  78. Slide № 191 diff --git src/app/ui/OptionsPlayer.jsx src/app/ui/OptionsPlayer.jsx index a2e1d2e..2333c9f 100644

    --- src/app/ui/OptionsPlayer.jsx +++ src/app/ui/OptionsPlayer.jsx @@ -1,176 +1,191 @@ const enhance = compose( connect((state) => ({ options: state.options, scratch: Options.scratchPosition(state.options), })), connectIO({ onSetPanel: () => (value) => OptionsIO.setOptions({ 'player.P1.panel': value }), onSetScratch: () => (position) => OptionsIO.setScratch(position), onSetSpeed: () => (speed) => OptionsIO.setSpeed(speed), onSetLeadTime: () => (leadTime) => OptionsIO.setLeadTime(leadTime), onSetLaneCover: () => (laneCover) => OptionsIO.setLaneCover(laneCover), onToggleBackgroundAnimationsEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'system.bga.enabled': Options.toggleOption(options['system.bga.enabled']) }) ), onToggleAutoVelocityEnabled: ({ options }) => () => ( OptionsIO.setOptions({ 'player.P1.auto-velocity': Options.toggleOption(options['player.P1.auto-velocity']) }) + ), + onToggleGauge: ({ options }) => () => ( + OptionsIO.setOptions({ + 'player.P1.gauge': Options.toggleGauge(options['player.P1.gauge']) + }) ) }), pure ) export const OptionsPlayer = React.createClass({ propTypes: { options: React.PropTypes.object, scratch: React.PropTypes.string, onClose: React.PropTypes.func, onSetPanel: React.PropTypes.func, onSetScratch: React.PropTypes.func, onSetSpeed: React.PropTypes.func, onSetLaneCover: React.PropTypes.func, onToggleBackgroundAnimationsEnabled: React.PropTypes.func, onToggleAutoVelocityEnabled: React.PropTypes.func, + onToggleGauge: React.PropTypes.func, onSetLeadTime: React.PropTypes.func }, render () { return <div className="OptionsPlayer"> <OptionsPlayer.Row label="Speed" hidden={Options.isAutoVelocityEnabled(this.props.options)} > <OptionsSpeed value={this.props.options['player.P1.speed']} onChange={this.props.onSetSpeed} /> <div className="OptionsPlayerͷhelp"> You can also change the speed in-game<br />using the Up and Down arrow keys. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="LeadTime" hidden={!Options.isAutoVelocityEnabled(this.props.options)} > <OptionsLeadTimeInputField value={Options.leadTime(this.props.options)} onChange={this.props.onSetLeadTime} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp"> Speed will be automatically adjusted<br />to maintain a consistent note velocity. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="Scratch"> <OptionsPlayerSelector type="scratch" options={SCRATCH_OPTIONS} onSelect={this.props.onSetScratch} value={this.props.scratch} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Panel"> <OptionsPlayerSelector type="panel" options={PANEL_OPTIONS} onSelect={this.props.onSetPanel} value={this.props.options['player.P1.panel']} /> </OptionsPlayer.Row> <OptionsPlayer.Row label="Cover"> <OptionsLaneCoverInputField value={Options.laneCover(this.props.options)} onChange={this.props.onSetLaneCover} style={{ width: '5em' }} /> <div className="OptionsPlayerͷhelp" title="Can be negative, in this case the play area is pulled up." > The amount of play area to hide from the top. </div> </OptionsPlayer.Row> <OptionsPlayer.Row label="BGA"> <OptionsCheckbox checked={Options.isBackgroundAnimationsEnabled(this.props.options)} onToggle={this.props.onToggleBackgroundAnimationsEnabled} > Enable background animations <span className="OptionsPlayerͷhint">(720p, alpha)</span> </OptionsCheckbox> </OptionsPlayer.Row> <OptionsPlayer.Row label="AutoVel"> <OptionsCheckbox checked={Options.isAutoVelocityEnabled(this.props.options)} onToggle={this.props.onToggleAutoVelocityEnabled} > Maintain absolute note velocity <span className="OptionsPlayerͷhint">(advanced)</span> </OptionsCheckbox> </OptionsPlayer.Row> + <OptionsPlayer.Row label="Gauge"> + <OptionsCheckbox + checked={Options.isGaugeEnabled(this.props.options)} + onToggle={this.props.onToggleGauge} + > + Show expert gauge <span className="OptionsPlayerͷhint">(experimental)</span> + </OptionsCheckbox> + </OptionsPlayer.Row> + <div className="OptionsPlayerͷbuttons"> <OptionsButton onClick={this.props.onClose}>Save & Exit</OptionsButton> </div> </div> } })
  79. Slide № 192 Cohesion loss Symptom: Making a simple change

    requires changing code that is further apart.
  80. Slide № 193 Cohesion loss Symptom: Reviewing a PR requires

    a lot of scrolling up and down to understand.
  81. Slide № 198 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func
  82. Slide № 199 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func Remove stuff not directly related to Navigation
  83. Slide № 200 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func
  84. Slide № 201 const enhance = connect( state => ({

    currentUser: selectCurrentUser(state), newNotificationCount: selectNewNotificationCount(state), newMessageCount: selectNewMessageCount(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout, onViewMessages: MessagesIO.viewMessages } ) class Navigation extends Component { static propTypes = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func
  85. Slide № 202 class Navigation extends Component { static propTypes

    = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem>
  86. Slide № 203 class Navigation extends Component { static propTypes

    = { currentUser: PropTypes.object, onLogin: PropTypes.func, onLogout: PropTypes.func, newNotificationCount: PropTypes.number, newMessageCount: PropTypes.number, onViewMessages: PropTypes.func } render () { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem>
  87. Slide № 204 class Navigation extends Component { render ()

    { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) </NavItem> } {loggedIn && <NavItem href='/inbox' onClick={this.props.onViewMessages}>
  88. Slide № 205 class Navigation extends Component { render ()

    { const loggedIn = !!this.props.currentUser return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> {loggedIn && <NavItem href='/notifications'> Notifications ({this.props.newNotificationCount}) </NavItem> } {loggedIn && <NavItem href='/inbox' onClick={this.props.onViewMessages}>
  89. Slide № 206 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> <NotificationsNavItem /> <MessagesNavItem />
  90. Slide № 207 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <LoginLogoutNavItem /> </Nav> </Navbar> )
  91. Slide № 208 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <LoginLogoutNavItem /> </Nav> </Navbar> )
  92. Slide № 211 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), )
  93. Slide № 212 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )
  94. Slide № 213 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  95. Slide № 214 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  96. Slide № 215 User Navigation Navigation const LoginLogoutNavItem = connect(

    state => ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  97. Slide № 216 User Navigation Navigation const LoginLogoutNavItem = connect(

    state => ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  98. Slide № 217 User Navigation Navigation const LoginLogoutNavItem = connect(

    state => ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  99. Slide № 218 User Navigation Navigation const LoginLogoutNavItem = connect(

    state => ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  100. Slide № 219 User Navigation Navigation const LoginLogoutNavItem = connect(

    state => ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) }) Delegate rendering logic
  101. Slide № 220 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( <NavItem href='#' onClick={props.onLogout}>Logout</NavItem> ) : ( <NavItem href='#' onClick={props.onLogin}>Login</NavItem> ) ) })
  102. Slide № 221 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) })
  103. Slide № 222 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) })
  104. Slide № 223 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) })
  105. Slide № 224 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) LoginLogoutNavItem.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  106. Slide № 225 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) LoginLogoutNavItem.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  107. Slide № 226 const LoginLogoutNavItem = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) LoginLogoutNavItem.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired } User Only concerned about current user and authentication Not concerned about how to render it
  108. Slide № 227 User const LoginLogoutNavItem = connect( state =>

    ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function LoginLogoutNavItem (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) LoginLogoutNavItem.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  109. Slide № 228 User const Auth = connect( state =>

    ({ currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  110. Slide № 229 const Auth = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  111. Slide № 230 const Auth = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired } “Render props”
  112. Slide № 232 class Navigation extends Component { render ()

    { return ( <Navbar> <Navbar.Header> <Navbar.Brand> <a href='#'>My app</a> </Navbar.Brand> </Navbar.Header> <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem> <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <LoginLogoutNavItem /> </Nav> </Navbar> )
  113. Slide № 233 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <LoginLogoutNavItem /> </Nav> </Navbar> ) } }
  114. Slide № 234 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <LoginLogoutNavItem /> </Nav> </Navbar> ) } }
  115. Slide № 235 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <Auth /> </Nav> </Navbar> ) } } Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  116. Slide № 236 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <Auth renderLoggedOut={ } renderLoggedIn={ } /> </Nav> </Navbar> ) } } Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  117. Slide № 237 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <Auth renderLoggedOut={(login) => <NavItem href='#' onClick={login}>Login</NavItem> } renderLoggedIn={(user, logout) => <NavItem href='#' onClick={logout}>Logout</NavItem> } /> </Nav> </Navbar> ) } } Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  118. Slide № 238 <Nav> <NavItem href='/'>Home</NavItem> <NavItem href='/explore'>Explore</NavItem> <NavItem href='/search'>Search</NavItem>

    <NotificationsNavItem /> <MessagesNavItem /> </Nav> <Nav pullRight> <Auth renderLoggedOut={(login) => <NavItem href='#' onClick={login}>Login</NavItem> } renderLoggedIn={(user, logout) => <NavItem href='#' onClick={logout}>Logout</NavItem> } /> </Nav> </Navbar> ) } } Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  119. Slide № 240 const Auth = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired }
  120. Slide № 241 const Auth = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired } Objective: Connect UI to data source.
  121. Slide № 242 const Auth = connect( state => ({

    currentUser: selectCurrentUser(state) }), { onLogin: LoginIO.showLoginModal, onLogout: LoginIO.logout } )(function Auth (props) { return ( props.currentUser ? ( props.renderLoggedIn(props.currentUser, props.onLogout) ) : ( props.renderLoggedOut(props.onLogin) ) ) }) Auth.propTypes = { renderLoggedIn: PropTypes.func.isRequired, renderLoggedOut: PropTypes.func.isRequired } “Data connector component”
  122. Slide № 251 I don’t know of any way to

    detect these smells automatically.
  123. Slide № 255 Not all smells are bad. Example: Duplicated

    code can sometimes be easier to read