Structuring React
components
Bartek Witczak
What do you mean by
structuring?
What do you mean by
structuring?
• single component in isolation
• relation between components
• structuring modules
• whole page architecture
What do you mean by
structuring?
• single component in isolation
• relation between components
• structuring modules
• whole page architecture
What’s the purpose?
What’s the purpose?
Product Library
What’s the purpose?
Product Library
React component
Props State
Lifecycle event
ContextRender
class WelcomeTo4Devs extends React.Component {
render () {
return 'Welcome to meme generator!'
}
}
(Functional) Stateless
Props State
Lifecycle event
ContextRender
const Loading = () => (
<div className='loading'>
<i className='icon-refresh spin’/>
</div>
)
Functional stateless
Functional stateless
const Loading = ({ text }) => (
<div className='loading'>
<i className='icon-refresh spin'/>
<div className='loading-text'>
{text}
</div>
</div>
)
Stateful
Props State
Lifecycle event
ContextRender
class UsersSelect extends React.Component {
constructor(props) {
super(props)
this.state = { loading: true, options: [] }
}
componentDidMount() {
UsersService.getAll().then(options => {
this.setState({ loading: false, options })
})
}
render() {
const { selected, onSelect } = this.props
const { loading, options } = this.state
return (
<Select
value={selected}
isLoading={loading}
options={options}
onChange={onSelect}
/>
)
}
}
Container & Presentational
Smart & Dumb
View & Controller
Container & Presentational
Props State
Lifecycle event
ContextRender
Props State
Lifecycle event
ContextRender
const UsersSelect = ({
onSelect,
options,
selected,
}) => (
<Select
value={selected}
options={options}
onChange={onSelect}
/>
)
Presentational
const mapStateToProps = state => ({
options: state.users.options,
selected: state.users.selected,
})
const mapDispatchToProps = dispatch => ({
onSelect: options => dispatch({
type: 'SELECT_USER',
options,
})
})
const Users = connect
(mapStateToProps, mapDispatchToProps)
(UsersSelect)
Container
Higher order component
const withLoading = WrappedComponent => ({ loading, ...props }) => {
if ( loading ) {
return <div>Keep calm and eat broccoli ...</div>
} else {
return <WrappedComponent {...props} />
}
}
Coupling & cohesion
Coupling
Dependency between elements
( components)
===
changing one
component does
not imply changes in
other
Loose coupling Tight coupling
changing one
component implies
changes in other
Cohesion
if element’s responsibilities form
one thing
===
single
responsibility
High cohesion Low cohesion
multiple
responsibilities
Loosely coupled
&
high cohesive component
Team
John Mike Tom
Challenge:
Create meme details panel
{
name: 'Challenge accepted',
file: 'challenge-accepted.jpg',
date: '2018-04-05T11:15:30',
creator: 'Jim Beam’,
}
const MemeDetails = ({ meme }) => (
<div className='meme-details'>
<div>Name: <span>{meme.name}</span></div>
<div>File: <span>{meme.file}</span></div>
<div>Date: <span>{format(meme.date, 'YYYY-MM-DD')}</span></div>
<div>Creator: <span>{meme.creator}</span></div>
</div>
)
John
...
<MemeDetails meme={meme} />
...
Mike
const MemeDetails = ({ name, file, date, creator }) => (
<div className='meme-details'>
<div>Name: <span>{name}</span></div>
<div>File: <span>{file}</span></div>
<div>Date: <span>{date}</span></div>
<div>Creator: <span>{creator}</span></div>
</div>
)
...
<MemeDetails
name={meme.name}
file={meme.file}
date={format(meme.date, ‘YYYY-MM-DD’)}
creator={meme.creator}
/>
...
Tom
const MemeDetails = ({ fields }) => (
<div className="meme-details">
{fields.map(f => (
<div>
{f.label}: <span>{f.value}</span>
</div>
))}
</div>
)
...
<MemeDetails
fields={[
{label: 'Name', value: meme.name},
{label: 'File', value: meme.file},
{label: 'Date', value: format(meme.date, 'YYYY-MM-DD')},
{label: 'Creator', value: meme.creator},
]}
/>
...
What about future
changes?
What about future
changes?
• changing model schema
const MemeDetails = ({ meme }) => (
<div className='meme-details'>
<div>Name: <span>{meme.name}</span></div>
<div>File: <span>{meme.file}</span></div>
<div>Format: <span>{meme.format}</span></div>
<div>Date: <span>{format(meme.date, 'YYYY-MM-DD')}</span></div>
<div>Creator: <span>{meme.creator}</span></div>
</div>
)
John
...
<MemeDetails meme={meme} />
...
Mike
const MemeDetails = ({ name, file, format, date, creator }) => (
<div className='meme-details'>
<div>Name: <span>{name}</span></div>
<div>File: <span>{file}</span></div>
<div>Format: <span>{format}</span></div>
<div>Date: <span>{date}</span></div>
<div>Creator: <span>{creator}</span></div>
</div>
)
...
<MemeDetails
name={meme.name}
file={meme.file}
format={meme.format}
date={format(meme.date, ‘YYYY-MM-DD’)}
creator={meme.creator}
/>
...
Tom
const MemeDetails = ({ fields }) => (
<div className="meme-details">
{fields.map(f => (
<div>
{o.label}: <span>{o.value}</span>
</div>
))}
</div>
)
...
<MemeDetails
fields={[
{label: 'Name', value: meme.name},
{label: 'File', value: meme.file},
{label: 'Format', value: meme.format},
{label: 'Date', value: format(meme.date, 'YYYY-MM-DD')},
{label: 'Creator', value: meme.creator},
]}
/>
...
What about future
changes?
• changing model schema
• formatting
const MemeDetails = ({ meme, dateFormat }) => (
<div className='meme-details'>
<div>Name: <span>{meme.name}</span></div>
<div>File: <span>{meme.file}</span></div>
<div>Date: <span>{format(meme.date, dateFormat)}</span></div>
<div>Creator: <span>{meme.creator}</span></div>
</div>
)
John
...
<MemeDetails meme={meme} meme=‘YYYY-MM-DD H:mm’/>
...
Mike
const MemeDetails = ({ name, file, date, creator }) => (
<div className='meme-details'>
<div>Name: <span>{name}</span></div>
<div>File: <span>{file}</span></div>
<div>Date: <span>{date}</span></div>
<div>Creator: <span>{creator}</span></div>
</div>
)
...
<MemeDetails
name={meme.name}
file={meme.file}
date={format(meme.date, ‘YYYY-MM-DD H:mm’)}
creator={meme.creator}
/>
...
Tom
const MemeDetails = ({ fields }) => (
<div className="meme-details">
{fields.map(f => (
<div>
{o.label}: <span>{o.value}</span>
</div>
))}
</div>
)
...
<MemeDetails
fields={[
{label: 'Name', value: meme.name},
{label: 'File', value: meme.file},
{label: 'Date', value:
format(meme.date, ‘YYYY-MM-DD H:mm’)},
{label: 'Creator', value: meme.creator},
]}
/>
...
Fight!
John Mike Tom
High cohesion
Tightly coupled with model
Single place when
modifying model
FormattingJohn
Loosely coupled with model
Formatting
Low cohesion
Many places when
modifying model
Mike
Very abstract
Loosely coupled with model
Low cohesion
Multiple places when
modifying model
Very abstract
Tom
And something from 

real world?
const OptionsList = ({ open, width, options = [], onOptionClick, range, height }) => !open || options.length === 0
? null
: (
<div style={{ width: width < 70 ? 70 : width, maxHeight: height }} className={classes2.optionsList}>
{
options.map(({ name, value, selected }, key) => (
<div
key={key}
className={classes2.optionsListItem + ' ' + (selected ? classes2.selected : '')}
onClick={e => {
e.stopPropagation()
onOptionClick(value, !selected)
}}>
{!range && <input type='checkbox' checked={selected} style={{ marginRight: '5px' }} />}
{name}
</div>
))
}
</div>
)
export class MultiSelect extends Component {
state = { optionsListOpen: false }
componentDidMount () {
const width = ReactDOM.findDOMNode(this).offsetWidth
this.setState({ width })
}
getInputText = () => {
const value = this.props.value || []
return value.length > 0 ? value.sort().join(', ') : 'Not selected'
}
onOptionClick = (selectedValue, selected) => {
const { onChange } = this.props
const value = this.props.value || []
onChange && onChange(selected
? [selectedValue, ...value]
: value.filter(item => selectedValue !== item)
)
}
toggleOptionsList = () => {
const { optionsListOpen } = this.state
this.setState({ optionsListOpen: !optionsListOpen })
}
prepareOptions = () => {
const { options } = this.props
const value = this.props.value || []
const preparedOptions = [...options]
value.forEach(selected => {
const optionIndex = preparedOptions.findIndex(({ value }) => value === selected)
if (optionIndex !== -1) preparedOptions[optionIndex] = { ...preparedOptions[optionIndex], selected: true }
})
return preparedOptions
}
close = () => this.setState({ optionsListOpen: false })
render () {
const { optionsListOpen, width } = this.state
const { options, inputSize, customStyle = {}, height } = this.props
return (
<ClickOutside onClickOutside={this.close}>
<div
className={classes.inputSelectForm}
style={{
position: 'relative',
display: 'flex',
height: inputSize || '44px',
alignItems: 'center',
...customStyle
}}
onClick={this.toggleOptionsList}>
<span style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'block',
fontSize: '15px'
}}>{this.getInputText()}</span>
<div style={{ position: 'absolute', right: 0}}>
<ChevronDown marginRight='0' />
</div>
<OptionsList
open={optionsListOpen}
width={width}
height={height}
options={this.prepareOptions()}
onOptionClick={this.onOptionClick} />
</div>
</ClickOutside>
)
}
}
export class RangeSelect extends Component {
state = { optionsListOpen: false, currentlyEditing: 'min' }
componentDidMount () {
const width = ReactDOM.findDOMNode(this).offsetWidth
this.setState({ width })
}
getParsedMinMax = () => ({
min: parseInt(this.props.min, 10),
max: parseInt(this.props.max, 10)
})
getInputText = () => {
const { min = 0, max = 0 } = this.getParsedMinMax()
if (!min && min !== 0) return '0'
return min === max ? max : `${min} - ${max}`
}
openOptionsList = () => {
const { optionsListOpen, currentlyEditing } = this.state
this.setState({ optionsListOpen: true })
}
prepareOptions = () => {
const { options } = this.props
const { min, max } = this.getParsedMinMax()
return options
.map(({ name, value }) => ({ name, value: parseInt(value, 10) }))
.map(option => ({ ...option, selected: option.value >= min && option.value <= max }))
}
onOptionClick = (selectedValue, selected) => {
const { onChange } = this.props
const { min, max } = this.getParsedMinMax()
const { currentlyEditing } = this.state
const parsedValue = parseInt(selectedValue, 10)
const newMinMax = { min: min.toString(), max: max.toString() }
if (currentlyEditing === 'min') {
newMinMax.min = parsedValue
newMinMax.max = parsedValue
this.setState({ currentlyEditing: 'max' })
} else {
if (parsedValue < min) {
newMinMax.max = min
newMinMax.min = parsedValue
} else {
newMinMax.max = parsedValue
}
this.setState({ currentlyEditing: 'min' })
}
onChange && onChange(newMinMax)
}
close = () => this.setState({ optionsListOpen: false, currentlyEditing: 'min' })
render () {
const { optionsListOpen, width } = this.state
const { options, inputSize, customStyle, height } = this.props
return (
<ClickOutside onClickOutside={this.close}>
<div
className={classes.inputSelectForm}
style={{
position: 'relative',
display: 'flex',
height: inputSize || '44px',
alignItems: 'center',
paddingRight: '0px',
...customStyle
}}
onClick={this.openOptionsList}>
<span style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'block',
fontSize: '15px'
}}>{this.getInputText()}</span>
<div style={{ position: 'absolute', right: 0}}>
<ChevronDown marginRight='0' />
</div>
<OptionsList
range
ref={optionsList => this.optionsList = optionsList}
open={optionsListOpen}
width={width}
height={height}
options={this.prepareOptions()}
onOptionClick={this.onOptionClick} />
</div>
</ClickOutside>
)
}
}
const typeInputs = {
text: (params, error, returnEvent) => (
<input
type='text'
{...params}
onChange={(e) => { (params && params.onChange) ? returnEvent ? params.onChange(e) : params.onChange(e.target.value) : null }}
className={`${classes.inputForm} ${error ? classes.inputError : ''} ${params.disabled ? classes.inputDisabled : ''} ${ (Object.keys(params.emptyStyle || {}).length && (!params.value || params.value === NOT_SELECTED)) ? classes.emptyInput : null }`} />
),
select: (params = {}, error) => {
const { options = [], onChange, prependNullOption, emptyStyle = {}} = params
const preparedOptions = prependNullOption ? [{ name: 'Not selected', value: null }, ...options] : options
const showEmptyStyle = Object.keys(emptyStyle).length && (!params.value || params.value === NOT_SELECTED)
return (
<select
disabled={params.disabled}
value={params.value}
onChange={e => {
const val = e.target.value
if (!onChange) return
if (typeof val === 'string') {
try {
const parsedVal = JSON.parse(val)
onChange && onChange(parsedVal)
} catch (e) {
onChange && onChange(val)
}
}
}}
className={`${classes.inputSelectForm} ${showEmptyStyle ? classes.emptyInput : null}`}>
{
preparedOptions.map((item, key) => (
<option
key={key}
value={item.value !== undefined ? item.value : item.name}>
{item.name}
</option>
))
}
</select>
)
},
multiselect: (params = {}, error) => (
<MultiSelect
options={params.options}
onChange={params.onChange}
value={params.value} />
),
range: (params = {}, error) => <RangeSelect {...params} />,
checkbox: (params, error) => (
<input
type='checkbox'
{...params}
/>
)
}
const renderInput = (type, params, error, returnEvent) => {
return typeInputs[type] ? typeInputs[type](params, error, returnEvent) : null
}
export const InputForm = ({ icon, type = 'text', params, error, labelText, labelStyle, labelType, customStyle, returnEvent }) => {
return (
<div
className={`${classes.inputContainer} ${type === 'select' || type === 'multiselect' ? classes.inputContainerWithIcon : ''}`}
style={!labelText ? { marginTop: '0', ...customStyle } : customStyle}>
{labelText && (<label className={`${classes[labelType] || classes.inputLabelForm} ${params.disabled ? classes.inputDisabled : ''}`} style={labelStyle} >{labelText}</label>)}
{renderInput(type, params, error, returnEvent)}
{icon && (<i className={`fa fa-${icon} ${classes.inputAddon} ${params.disabled ? classes.inputDisabled : ''}`} />)}
</div>
)
}
export default InputForm
export const NOT_SELECTED = 'not_selected'
export const validateSelected = (property) => {
return property && property !== NOT_SELECTED
}
INPUT
5 types
• text
• select
• multiselect
• range
• checkbox
9 props
params {} 

can match any type
very-very-very-generic
both controlled & uncontrolled
at the same time
I thought GENERIC is a good thing
Mission 2
Access rights
Keep calm,

start small,
refactor in need.
class MemePage extends React.Component {
...
render() {
const { permissions } = this.props
return (
<div>
{
Permissions.canCreateMeme(permissions) ? (
<MemeGeneratorLink />
) : null
}
{/* RENDER MEMES */}
</div>
)
}
}
const mapStateToProps = state => ({ permissions: state.permission })
export default connect(mapStateToProps)(MemePage)
• create meme?
• edit meme?
• delete meme?
• see meme history?
• generate meme for
presentation?
• download meme?
const HasPermission = ({ children, permissions, role}) => {
if (role === 'CREATE_MEME' && Permissions.canCreateMeme(permissions)) {
return children
}
return null
}
const mapStateToProps = state => ({ permissions: state.permission })
export default connect(mapStateToProps)(HasPermission)
class MemesPage extends React.Component {
...
render() {
return (
<div>
<HasPermission role='CREATE_MEME'>
<MemeGeneratorLink />
</HasPermission>
{/* RENDER MEMES */}
</div>
)
}
}
const mapStateToProps = state => ({ permissions: state.permission })
const withPermission = (WrappedComponent, role) => 

connect(mapStateToProps)(({ permissions, ...props }) => {
if (role === 'CREATE_MEME' && Permissions.canCreateMeme(permissions)) {
return <WrappedComponent {...props}/>
}
return null
}
))
export withPermission(MemeGeneratorLink, ‘CREATE_MEME’)
///
class MemesPage extends React.Component {
...
render() {
return (
<div>
<MemeGeneratorLink />
{/* RENDER MEMES */}
</div>
)
}
}
Children 

/ 

HOC 

/

nothing
?
Criteria
• better abstraction
• separation of concerns
• improved maintainability
• better reusability
Context is the king
• project
• stage of project
• component
• developer preferences
• requirements
“We value code that is easy to maintain 

over code that is easy to write”
Nat Pryce, Steve Freeman
Bartek Witczak
@bartekwitczak
bartek@dayone.pl

Structuring React.js Components

  • 1.
  • 2.
    What do youmean by structuring?
  • 3.
    What do youmean by structuring? • single component in isolation • relation between components • structuring modules • whole page architecture
  • 4.
    What do youmean by structuring? • single component in isolation • relation between components • structuring modules • whole page architecture
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    class WelcomeTo4Devs extendsReact.Component { render () { return 'Welcome to meme generator!' } }
  • 10.
  • 11.
    const Loading =() => ( <div className='loading'> <i className='icon-refresh spin’/> </div> ) Functional stateless
  • 12.
    Functional stateless const Loading= ({ text }) => ( <div className='loading'> <i className='icon-refresh spin'/> <div className='loading-text'> {text} </div> </div> )
  • 13.
  • 14.
    class UsersSelect extendsReact.Component { constructor(props) { super(props) this.state = { loading: true, options: [] } } componentDidMount() { UsersService.getAll().then(options => { this.setState({ loading: false, options }) }) } render() { const { selected, onSelect } = this.props const { loading, options } = this.state return ( <Select value={selected} isLoading={loading} options={options} onChange={onSelect} /> ) } }
  • 15.
    Container & Presentational Smart& Dumb View & Controller
  • 16.
    Container & Presentational PropsState Lifecycle event ContextRender Props State Lifecycle event ContextRender
  • 17.
    const UsersSelect =({ onSelect, options, selected, }) => ( <Select value={selected} options={options} onChange={onSelect} /> ) Presentational const mapStateToProps = state => ({ options: state.users.options, selected: state.users.selected, }) const mapDispatchToProps = dispatch => ({ onSelect: options => dispatch({ type: 'SELECT_USER', options, }) }) const Users = connect (mapStateToProps, mapDispatchToProps) (UsersSelect) Container
  • 18.
  • 19.
    const withLoading =WrappedComponent => ({ loading, ...props }) => { if ( loading ) { return <div>Keep calm and eat broccoli ...</div> } else { return <WrappedComponent {...props} /> } }
  • 20.
  • 21.
  • 22.
    changing one component does notimply changes in other Loose coupling Tight coupling changing one component implies changes in other
  • 23.
  • 24.
    single responsibility High cohesion Lowcohesion multiple responsibilities
  • 25.
  • 26.
  • 27.
    Challenge: Create meme detailspanel { name: 'Challenge accepted', file: 'challenge-accepted.jpg', date: '2018-04-05T11:15:30', creator: 'Jim Beam’, }
  • 28.
    const MemeDetails =({ meme }) => ( <div className='meme-details'> <div>Name: <span>{meme.name}</span></div> <div>File: <span>{meme.file}</span></div> <div>Date: <span>{format(meme.date, 'YYYY-MM-DD')}</span></div> <div>Creator: <span>{meme.creator}</span></div> </div> ) John ... <MemeDetails meme={meme} /> ...
  • 29.
    Mike const MemeDetails =({ name, file, date, creator }) => ( <div className='meme-details'> <div>Name: <span>{name}</span></div> <div>File: <span>{file}</span></div> <div>Date: <span>{date}</span></div> <div>Creator: <span>{creator}</span></div> </div> ) ... <MemeDetails name={meme.name} file={meme.file} date={format(meme.date, ‘YYYY-MM-DD’)} creator={meme.creator} /> ...
  • 30.
    Tom const MemeDetails =({ fields }) => ( <div className="meme-details"> {fields.map(f => ( <div> {f.label}: <span>{f.value}</span> </div> ))} </div> ) ... <MemeDetails fields={[ {label: 'Name', value: meme.name}, {label: 'File', value: meme.file}, {label: 'Date', value: format(meme.date, 'YYYY-MM-DD')}, {label: 'Creator', value: meme.creator}, ]} /> ...
  • 31.
  • 32.
    What about future changes? •changing model schema
  • 33.
    const MemeDetails =({ meme }) => ( <div className='meme-details'> <div>Name: <span>{meme.name}</span></div> <div>File: <span>{meme.file}</span></div> <div>Format: <span>{meme.format}</span></div> <div>Date: <span>{format(meme.date, 'YYYY-MM-DD')}</span></div> <div>Creator: <span>{meme.creator}</span></div> </div> ) John ... <MemeDetails meme={meme} /> ...
  • 34.
    Mike const MemeDetails =({ name, file, format, date, creator }) => ( <div className='meme-details'> <div>Name: <span>{name}</span></div> <div>File: <span>{file}</span></div> <div>Format: <span>{format}</span></div> <div>Date: <span>{date}</span></div> <div>Creator: <span>{creator}</span></div> </div> ) ... <MemeDetails name={meme.name} file={meme.file} format={meme.format} date={format(meme.date, ‘YYYY-MM-DD’)} creator={meme.creator} /> ...
  • 35.
    Tom const MemeDetails =({ fields }) => ( <div className="meme-details"> {fields.map(f => ( <div> {o.label}: <span>{o.value}</span> </div> ))} </div> ) ... <MemeDetails fields={[ {label: 'Name', value: meme.name}, {label: 'File', value: meme.file}, {label: 'Format', value: meme.format}, {label: 'Date', value: format(meme.date, 'YYYY-MM-DD')}, {label: 'Creator', value: meme.creator}, ]} /> ...
  • 36.
    What about future changes? •changing model schema • formatting
  • 37.
    const MemeDetails =({ meme, dateFormat }) => ( <div className='meme-details'> <div>Name: <span>{meme.name}</span></div> <div>File: <span>{meme.file}</span></div> <div>Date: <span>{format(meme.date, dateFormat)}</span></div> <div>Creator: <span>{meme.creator}</span></div> </div> ) John ... <MemeDetails meme={meme} meme=‘YYYY-MM-DD H:mm’/> ...
  • 38.
    Mike const MemeDetails =({ name, file, date, creator }) => ( <div className='meme-details'> <div>Name: <span>{name}</span></div> <div>File: <span>{file}</span></div> <div>Date: <span>{date}</span></div> <div>Creator: <span>{creator}</span></div> </div> ) ... <MemeDetails name={meme.name} file={meme.file} date={format(meme.date, ‘YYYY-MM-DD H:mm’)} creator={meme.creator} /> ...
  • 39.
    Tom const MemeDetails =({ fields }) => ( <div className="meme-details"> {fields.map(f => ( <div> {o.label}: <span>{o.value}</span> </div> ))} </div> ) ... <MemeDetails fields={[ {label: 'Name', value: meme.name}, {label: 'File', value: meme.file}, {label: 'Date', value: format(meme.date, ‘YYYY-MM-DD H:mm’)}, {label: 'Creator', value: meme.creator}, ]} /> ...
  • 40.
  • 41.
    High cohesion Tightly coupledwith model Single place when modifying model FormattingJohn
  • 42.
    Loosely coupled withmodel Formatting Low cohesion Many places when modifying model Mike
  • 43.
    Very abstract Loosely coupledwith model Low cohesion Multiple places when modifying model Very abstract Tom
  • 44.
    And something from
 real world?
  • 45.
    const OptionsList =({ open, width, options = [], onOptionClick, range, height }) => !open || options.length === 0 ? null : ( <div style={{ width: width < 70 ? 70 : width, maxHeight: height }} className={classes2.optionsList}> { options.map(({ name, value, selected }, key) => ( <div key={key} className={classes2.optionsListItem + ' ' + (selected ? classes2.selected : '')} onClick={e => { e.stopPropagation() onOptionClick(value, !selected) }}> {!range && <input type='checkbox' checked={selected} style={{ marginRight: '5px' }} />} {name} </div> )) } </div> ) export class MultiSelect extends Component { state = { optionsListOpen: false } componentDidMount () { const width = ReactDOM.findDOMNode(this).offsetWidth this.setState({ width }) } getInputText = () => { const value = this.props.value || [] return value.length > 0 ? value.sort().join(', ') : 'Not selected' } onOptionClick = (selectedValue, selected) => { const { onChange } = this.props const value = this.props.value || [] onChange && onChange(selected ? [selectedValue, ...value] : value.filter(item => selectedValue !== item) ) } toggleOptionsList = () => { const { optionsListOpen } = this.state this.setState({ optionsListOpen: !optionsListOpen }) } prepareOptions = () => { const { options } = this.props const value = this.props.value || [] const preparedOptions = [...options] value.forEach(selected => { const optionIndex = preparedOptions.findIndex(({ value }) => value === selected) if (optionIndex !== -1) preparedOptions[optionIndex] = { ...preparedOptions[optionIndex], selected: true } }) return preparedOptions } close = () => this.setState({ optionsListOpen: false }) render () { const { optionsListOpen, width } = this.state const { options, inputSize, customStyle = {}, height } = this.props return ( <ClickOutside onClickOutside={this.close}> <div className={classes.inputSelectForm} style={{ position: 'relative', display: 'flex', height: inputSize || '44px', alignItems: 'center', ...customStyle }} onClick={this.toggleOptionsList}> <span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', display: 'block', fontSize: '15px' }}>{this.getInputText()}</span> <div style={{ position: 'absolute', right: 0}}> <ChevronDown marginRight='0' /> </div> <OptionsList open={optionsListOpen} width={width} height={height} options={this.prepareOptions()} onOptionClick={this.onOptionClick} /> </div> </ClickOutside> ) } } export class RangeSelect extends Component { state = { optionsListOpen: false, currentlyEditing: 'min' } componentDidMount () { const width = ReactDOM.findDOMNode(this).offsetWidth this.setState({ width }) } getParsedMinMax = () => ({ min: parseInt(this.props.min, 10), max: parseInt(this.props.max, 10) }) getInputText = () => { const { min = 0, max = 0 } = this.getParsedMinMax() if (!min && min !== 0) return '0' return min === max ? max : `${min} - ${max}` } openOptionsList = () => { const { optionsListOpen, currentlyEditing } = this.state this.setState({ optionsListOpen: true }) } prepareOptions = () => { const { options } = this.props const { min, max } = this.getParsedMinMax() return options .map(({ name, value }) => ({ name, value: parseInt(value, 10) })) .map(option => ({ ...option, selected: option.value >= min && option.value <= max })) } onOptionClick = (selectedValue, selected) => { const { onChange } = this.props const { min, max } = this.getParsedMinMax() const { currentlyEditing } = this.state const parsedValue = parseInt(selectedValue, 10) const newMinMax = { min: min.toString(), max: max.toString() } if (currentlyEditing === 'min') { newMinMax.min = parsedValue newMinMax.max = parsedValue this.setState({ currentlyEditing: 'max' }) } else { if (parsedValue < min) { newMinMax.max = min newMinMax.min = parsedValue } else { newMinMax.max = parsedValue } this.setState({ currentlyEditing: 'min' }) } onChange && onChange(newMinMax) } close = () => this.setState({ optionsListOpen: false, currentlyEditing: 'min' }) render () { const { optionsListOpen, width } = this.state const { options, inputSize, customStyle, height } = this.props return ( <ClickOutside onClickOutside={this.close}> <div className={classes.inputSelectForm} style={{ position: 'relative', display: 'flex', height: inputSize || '44px', alignItems: 'center', paddingRight: '0px', ...customStyle }} onClick={this.openOptionsList}> <span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', display: 'block', fontSize: '15px' }}>{this.getInputText()}</span> <div style={{ position: 'absolute', right: 0}}> <ChevronDown marginRight='0' /> </div> <OptionsList range ref={optionsList => this.optionsList = optionsList} open={optionsListOpen} width={width} height={height} options={this.prepareOptions()} onOptionClick={this.onOptionClick} /> </div> </ClickOutside> ) } } const typeInputs = { text: (params, error, returnEvent) => ( <input type='text' {...params} onChange={(e) => { (params && params.onChange) ? returnEvent ? params.onChange(e) : params.onChange(e.target.value) : null }} className={`${classes.inputForm} ${error ? classes.inputError : ''} ${params.disabled ? classes.inputDisabled : ''} ${ (Object.keys(params.emptyStyle || {}).length && (!params.value || params.value === NOT_SELECTED)) ? classes.emptyInput : null }`} /> ), select: (params = {}, error) => { const { options = [], onChange, prependNullOption, emptyStyle = {}} = params const preparedOptions = prependNullOption ? [{ name: 'Not selected', value: null }, ...options] : options const showEmptyStyle = Object.keys(emptyStyle).length && (!params.value || params.value === NOT_SELECTED) return ( <select disabled={params.disabled} value={params.value} onChange={e => { const val = e.target.value if (!onChange) return if (typeof val === 'string') { try { const parsedVal = JSON.parse(val) onChange && onChange(parsedVal) } catch (e) { onChange && onChange(val) } } }} className={`${classes.inputSelectForm} ${showEmptyStyle ? classes.emptyInput : null}`}> { preparedOptions.map((item, key) => ( <option key={key} value={item.value !== undefined ? item.value : item.name}> {item.name} </option> )) } </select> ) }, multiselect: (params = {}, error) => ( <MultiSelect options={params.options} onChange={params.onChange} value={params.value} /> ), range: (params = {}, error) => <RangeSelect {...params} />, checkbox: (params, error) => ( <input type='checkbox' {...params} /> ) } const renderInput = (type, params, error, returnEvent) => { return typeInputs[type] ? typeInputs[type](params, error, returnEvent) : null } export const InputForm = ({ icon, type = 'text', params, error, labelText, labelStyle, labelType, customStyle, returnEvent }) => { return ( <div className={`${classes.inputContainer} ${type === 'select' || type === 'multiselect' ? classes.inputContainerWithIcon : ''}`} style={!labelText ? { marginTop: '0', ...customStyle } : customStyle}> {labelText && (<label className={`${classes[labelType] || classes.inputLabelForm} ${params.disabled ? classes.inputDisabled : ''}`} style={labelStyle} >{labelText}</label>)} {renderInput(type, params, error, returnEvent)} {icon && (<i className={`fa fa-${icon} ${classes.inputAddon} ${params.disabled ? classes.inputDisabled : ''}`} />)} </div> ) } export default InputForm export const NOT_SELECTED = 'not_selected' export const validateSelected = (property) => { return property && property !== NOT_SELECTED }
  • 46.
    INPUT 5 types • text •select • multiselect • range • checkbox 9 props params {} 
 can match any type very-very-very-generic both controlled & uncontrolled at the same time
  • 47.
    I thought GENERICis a good thing
  • 48.
  • 49.
  • 50.
  • 51.
    class MemePage extendsReact.Component { ... render() { const { permissions } = this.props return ( <div> { Permissions.canCreateMeme(permissions) ? ( <MemeGeneratorLink /> ) : null } {/* RENDER MEMES */} </div> ) } } const mapStateToProps = state => ({ permissions: state.permission }) export default connect(mapStateToProps)(MemePage)
  • 52.
    • create meme? •edit meme? • delete meme? • see meme history? • generate meme for presentation? • download meme?
  • 53.
    const HasPermission =({ children, permissions, role}) => { if (role === 'CREATE_MEME' && Permissions.canCreateMeme(permissions)) { return children } return null } const mapStateToProps = state => ({ permissions: state.permission }) export default connect(mapStateToProps)(HasPermission)
  • 54.
    class MemesPage extendsReact.Component { ... render() { return ( <div> <HasPermission role='CREATE_MEME'> <MemeGeneratorLink /> </HasPermission> {/* RENDER MEMES */} </div> ) } }
  • 55.
    const mapStateToProps =state => ({ permissions: state.permission }) const withPermission = (WrappedComponent, role) => 
 connect(mapStateToProps)(({ permissions, ...props }) => { if (role === 'CREATE_MEME' && Permissions.canCreateMeme(permissions)) { return <WrappedComponent {...props}/> } return null } ))
  • 56.
    export withPermission(MemeGeneratorLink, ‘CREATE_MEME’) /// classMemesPage extends React.Component { ... render() { return ( <div> <MemeGeneratorLink /> {/* RENDER MEMES */} </div> ) } }
  • 57.
    Children 
 / 
 HOC
 /
 nothing ?
  • 58.
    Criteria • better abstraction •separation of concerns • improved maintainability • better reusability
  • 59.
    Context is theking • project • stage of project • component • developer preferences • requirements
  • 60.
    “We value codethat is easy to maintain 
 over code that is easy to write” Nat Pryce, Steve Freeman Bartek Witczak @bartekwitczak bartek@dayone.pl