SlideShare a Scribd company logo
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
4Developers 2018: Structuring React components (Bartłomiej Witczak)
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

More Related Content

What's hot (16)

PDF
GQL cheat sheet latest
sones GmbH
 
PDF
Building iPhone Web Apps using "classic" Domino
Rob Bontekoe
 
PDF
Chris Holland "Leveraging Typed Exceptions for Cleaner Error Handling"
Fwdays
 
PDF
React.js: Beyond the Browser
garbles
 
PDF
Functionality Focused Code Organization
Rebecca Murphey
 
PDF
Ms Ajax Dom Element Class
jason hu 金良胡
 
PDF
Delivering a Responsive UI
Rebecca Murphey
 
PDF
Patterns / Antipatterns with NoSQL
Luca Bonmassar
 
PPTX
Rapid and Scalable Development with MongoDB, PyMongo, and Ming
Rick Copeland
 
PDF
Drupal Entities - Emerging Patterns of Usage
Ronald Ashri
 
PPTX
JavaScript Objects and OOP Programming with JavaScript
Laurence Svekis ✔
 
PDF
Hibernate
Leonardo Passos
 
PDF
Dig Deeper into WordPress - WD Meetup Cairo
Mohamed Mosaad
 
PPTX
HirshHorn theme: how I created it
Paul Bearne
 
PPTX
Windows 8 JavaScript (Wonderland)
Christopher Bennage
 
GQL cheat sheet latest
sones GmbH
 
Building iPhone Web Apps using "classic" Domino
Rob Bontekoe
 
Chris Holland "Leveraging Typed Exceptions for Cleaner Error Handling"
Fwdays
 
React.js: Beyond the Browser
garbles
 
Functionality Focused Code Organization
Rebecca Murphey
 
Ms Ajax Dom Element Class
jason hu 金良胡
 
Delivering a Responsive UI
Rebecca Murphey
 
Patterns / Antipatterns with NoSQL
Luca Bonmassar
 
Rapid and Scalable Development with MongoDB, PyMongo, and Ming
Rick Copeland
 
Drupal Entities - Emerging Patterns of Usage
Ronald Ashri
 
JavaScript Objects and OOP Programming with JavaScript
Laurence Svekis ✔
 
Hibernate
Leonardo Passos
 
Dig Deeper into WordPress - WD Meetup Cairo
Mohamed Mosaad
 
HirshHorn theme: how I created it
Paul Bearne
 
Windows 8 JavaScript (Wonderland)
Christopher Bennage
 

Similar to 4Developers 2018: Structuring React components (Bartłomiej Witczak) (20)

PDF
Strategies for Mitigating Complexity in React Based Redux Applicaitons
garbles
 
PDF
Higher-Order Components — Ilya Gelman
500Tech
 
PDF
React for Dummies
Mitch Chen
 
PDF
Workshop 19: ReactJS Introduction
Visual Engineering
 
PDF
JS Fest 2019. Glenn Reyes. With great power comes great React hooks!
JSFestUA
 
PDF
Backbone to React. What it says about awesome UI Code.
Richard Powell
 
PDF
2018 05-16 Evolving Technologies: React, Babel & Webpack
Codifly
 
PDF
Stay with React.js in 2020
Jerry Liao
 
PDF
ReactでGraphQLを使っている
Takahiro Kobaru
 
PDF
Basic Tutorial of React for Programmers
David Rodenas
 
PDF
Redux Deep Dive - ReactFoo Pune 2018
Aziz Khambati
 
PDF
3 Simple Steps to follow to Create React JS Components
Surendra kumar
 
PPTX
GraphQL, Redux, and React
Keon Kim
 
PPTX
React 16: new features and beyond
Artjoker
 
PDF
Making react part of something greater
Darko Kukovec
 
PPTX
React & redux
Cédric Hartland
 
PPTX
React responsively, render responsibly - react meetup
Yoav Niran
 
PDF
ES6 patterns in the wild
Joe Morgan
 
PDF
Reactjs: Rethinking UI Devel
Stefano Ceschi Berrini
 
PDF
Why react matters
ShihChi Huang
 
Strategies for Mitigating Complexity in React Based Redux Applicaitons
garbles
 
Higher-Order Components — Ilya Gelman
500Tech
 
React for Dummies
Mitch Chen
 
Workshop 19: ReactJS Introduction
Visual Engineering
 
JS Fest 2019. Glenn Reyes. With great power comes great React hooks!
JSFestUA
 
Backbone to React. What it says about awesome UI Code.
Richard Powell
 
2018 05-16 Evolving Technologies: React, Babel & Webpack
Codifly
 
Stay with React.js in 2020
Jerry Liao
 
ReactでGraphQLを使っている
Takahiro Kobaru
 
Basic Tutorial of React for Programmers
David Rodenas
 
Redux Deep Dive - ReactFoo Pune 2018
Aziz Khambati
 
3 Simple Steps to follow to Create React JS Components
Surendra kumar
 
GraphQL, Redux, and React
Keon Kim
 
React 16: new features and beyond
Artjoker
 
Making react part of something greater
Darko Kukovec
 
React & redux
Cédric Hartland
 
React responsively, render responsibly - react meetup
Yoav Niran
 
ES6 patterns in the wild
Joe Morgan
 
Reactjs: Rethinking UI Devel
Stefano Ceschi Berrini
 
Why react matters
ShihChi Huang
 
Ad

Recently uploaded (20)

PDF
Complete JavaScript Notes: From Basics to Advanced Concepts.pdf
haydendavispro
 
PDF
HCIP-Data Center Facility Deployment V2.0 Training Material (Without Remarks ...
mcastillo49
 
PPTX
Top Managed Service Providers in Los Angeles
Captain IT
 
PDF
Chris Elwell Woburn, MA - Passionate About IT Innovation
Chris Elwell Woburn, MA
 
PPTX
WooCommerce Workshop: Bring Your Laptop
Laura Hartwig
 
PDF
CIFDAQ Token Spotlight for 9th July 2025
CIFDAQ
 
PDF
NewMind AI - Journal 100 Insights After The 100th Issue
NewMind AI
 
PDF
Persuasive AI: risks and opportunities in the age of digital debate
Speck&Tech
 
PDF
Why Orbit Edge Tech is a Top Next JS Development Company in 2025
mahendraalaska08
 
PDF
Empowering Cloud Providers with Apache CloudStack and Stackbill
ShapeBlue
 
PDF
Meetup Kickoff & Welcome - Rohit Yadav, CSIUG Chairman
ShapeBlue
 
PDF
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
PPTX
Building Search Using OpenSearch: Limitations and Workarounds
Sease
 
PDF
July Patch Tuesday
Ivanti
 
PDF
Ampere Offers Energy-Efficient Future For AI And Cloud
ShapeBlue
 
PDF
SWEBOK Guide and Software Services Engineering Education
Hironori Washizaki
 
PDF
NewMind AI Journal - Weekly Chronicles - July'25 Week II
NewMind AI
 
PPTX
Darren Mills The Migration Modernization Balancing Act: Navigating Risks and...
AWS Chicago
 
PPTX
Top iOS App Development Company in the USA for Innovative Apps
SynapseIndia
 
PPTX
✨Unleashing Collaboration: Salesforce Channels & Community Power in Patna!✨
SanjeetMishra29
 
Complete JavaScript Notes: From Basics to Advanced Concepts.pdf
haydendavispro
 
HCIP-Data Center Facility Deployment V2.0 Training Material (Without Remarks ...
mcastillo49
 
Top Managed Service Providers in Los Angeles
Captain IT
 
Chris Elwell Woburn, MA - Passionate About IT Innovation
Chris Elwell Woburn, MA
 
WooCommerce Workshop: Bring Your Laptop
Laura Hartwig
 
CIFDAQ Token Spotlight for 9th July 2025
CIFDAQ
 
NewMind AI - Journal 100 Insights After The 100th Issue
NewMind AI
 
Persuasive AI: risks and opportunities in the age of digital debate
Speck&Tech
 
Why Orbit Edge Tech is a Top Next JS Development Company in 2025
mahendraalaska08
 
Empowering Cloud Providers with Apache CloudStack and Stackbill
ShapeBlue
 
Meetup Kickoff & Welcome - Rohit Yadav, CSIUG Chairman
ShapeBlue
 
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
Building Search Using OpenSearch: Limitations and Workarounds
Sease
 
July Patch Tuesday
Ivanti
 
Ampere Offers Energy-Efficient Future For AI And Cloud
ShapeBlue
 
SWEBOK Guide and Software Services Engineering Education
Hironori Washizaki
 
NewMind AI Journal - Weekly Chronicles - July'25 Week II
NewMind AI
 
Darren Mills The Migration Modernization Balancing Act: Navigating Risks and...
AWS Chicago
 
Top iOS App Development Company in the USA for Innovative Apps
SynapseIndia
 
✨Unleashing Collaboration: Salesforce Channels & Community Power in Patna!✨
SanjeetMishra29
 
Ad

4Developers 2018: Structuring React components (Bartłomiej Witczak)

  • 2. What do you mean by structuring?
  • 3. What do you mean by structuring? • single component in isolation • relation between components • structuring modules • whole page architecture
  • 4. What do you mean by structuring? • single component in isolation • relation between components • structuring modules • whole page architecture
  • 9. class WelcomeTo4Devs extends React.Component { render () { return 'Welcome to meme generator!' } }
  • 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> )
  • 14. 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} /> ) } }
  • 15. Container & Presentational Smart & Dumb View & Controller
  • 16. Container & Presentational Props State 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
  • 19. const withLoading = WrappedComponent => ({ loading, ...props }) => { if ( loading ) { return <div>Keep calm and eat broccoli ...</div> } else { return <WrappedComponent {...props} /> } }
  • 22. changing one component does not imply changes in other Loose coupling Tight coupling changing one component implies changes in other
  • 24. single responsibility High cohesion Low cohesion multiple responsibilities
  • 27. Challenge: Create meme details panel { 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}, ]} /> ...
  • 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}, ]} /> ...
  • 41. High cohesion Tightly coupled with model Single place when modifying model FormattingJohn
  • 42. Loosely coupled with model Formatting Low cohesion Many places when modifying model Mike
  • 43. Very abstract Loosely coupled with 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
  • 48. I thought GENERIC is a good thing
  • 52. 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)
  • 53. • create meme? • edit meme? • delete meme? • see meme history? • generate meme for presentation? • download meme?
  • 54. 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)
  • 55. class MemesPage extends React.Component { ... render() { return ( <div> <HasPermission role='CREATE_MEME'> <MemeGeneratorLink /> </HasPermission> {/* RENDER MEMES */} </div> ) } }
  • 56. 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 } ))
  • 57. export withPermission(MemeGeneratorLink, ‘CREATE_MEME’) /// class MemesPage extends React.Component { ... render() { return ( <div> <MemeGeneratorLink /> {/* RENDER MEMES */} </div> ) } }
  • 58. Children 
 / 
 HOC 
 /
 nothing ?
  • 59. Criteria • better abstraction • separation of concerns • improved maintainability • better reusability
  • 60. Context is the king • project • stage of project • component • developer preferences • requirements
  • 61. “We value code that is easy to maintain 
 over code that is easy to write” Nat Pryce, Steve Freeman Bartek Witczak @bartekwitczak [email protected]