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
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 Structuring React.js Components (20)

PDF
How to Mess Up Your Angular UI Components
cagataycivici
 
PDF
준비하세요 Angular js 2.0
Jeado Ko
 
PPTX
Building complex User Interfaces with Sitecore and React
Jonne Kats
 
PPTX
Getting the Most Out of jQuery Widgets
velveeta_512
 
PDF
Architecture Components In Real Life Season 2
Somkiat Khitwongwattana
 
PDF
React.js: You deserve to know about it
Anderson Aguiar
 
PPTX
Big Data for each one of us
OSCON Byrum
 
PPTX
How to increase Performance of Web Application using JQuery
kolkatageeks
 
PDF
Clean Javascript
Ryunosuke SATO
 
PPT
J query b_dotnet_ug_meet_12_may_2012
ghnash
 
PDF
Redux vs Alt
Uldis Sturms
 
PPTX
Knockoutjs UG meeting presentation
Valdis Iljuconoks
 
PDF
Understanding backbonejs
Nick Lee
 
PDF
Separation of concerns - DPC12
Stephan Hochdörfer
 
PDF
jQuery Rescue Adventure
Allegient
 
PDF
Local storage in Web apps
Ivano Malavolta
 
PDF
Web components with java by Haijian Wang
GWTcon
 
PPTX
jQuery
Dileep Mishra
 
PPTX
SenchaCon 2016: Ext JS + React: A Match Made in UX Heaven - Mark Brocato
Sencha
 
PDF
Viastudy ef core_cheat_sheet
imdurgesh
 
How to Mess Up Your Angular UI Components
cagataycivici
 
준비하세요 Angular js 2.0
Jeado Ko
 
Building complex User Interfaces with Sitecore and React
Jonne Kats
 
Getting the Most Out of jQuery Widgets
velveeta_512
 
Architecture Components In Real Life Season 2
Somkiat Khitwongwattana
 
React.js: You deserve to know about it
Anderson Aguiar
 
Big Data for each one of us
OSCON Byrum
 
How to increase Performance of Web Application using JQuery
kolkatageeks
 
Clean Javascript
Ryunosuke SATO
 
J query b_dotnet_ug_meet_12_may_2012
ghnash
 
Redux vs Alt
Uldis Sturms
 
Knockoutjs UG meeting presentation
Valdis Iljuconoks
 
Understanding backbonejs
Nick Lee
 
Separation of concerns - DPC12
Stephan Hochdörfer
 
jQuery Rescue Adventure
Allegient
 
Local storage in Web apps
Ivano Malavolta
 
Web components with java by Haijian Wang
GWTcon
 
SenchaCon 2016: Ext JS + React: A Match Made in UX Heaven - Mark Brocato
Sencha
 
Viastudy ef core_cheat_sheet
imdurgesh
 
Ad

Recently uploaded (20)

PDF
Rethinking Security Operations - SOC Evolution Journey.pdf
Haris Chughtai
 
PPTX
✨Unleashing Collaboration: Salesforce Channels & Community Power in Patna!✨
SanjeetMishra29
 
PDF
The Builder’s Playbook - 2025 State of AI Report.pdf
jeroen339954
 
PDF
SFWelly Summer 25 Release Highlights July 2025
Anna Loughnan Colquhoun
 
PPTX
Building a Production-Ready Barts Health Secure Data Environment Tooling, Acc...
Barts Health
 
PDF
Apache CloudStack 201: Let's Design & Build an IaaS Cloud
ShapeBlue
 
PDF
CloudStack GPU Integration - Rohit Yadav
ShapeBlue
 
PDF
TrustArc Webinar - Data Privacy Trends 2025: Mid-Year Insights & Program Stra...
TrustArc
 
PDF
CIFDAQ Token Spotlight for 9th July 2025
CIFDAQ
 
PDF
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
PDF
Wojciech Ciemski for Top Cyber News MAGAZINE. June 2025
Dr. Ludmila Morozova-Buss
 
PDF
LLMs.txt: Easily Control How AI Crawls Your Site
Keploy
 
PDF
Windsurf Meetup Ottawa 2025-07-12 - Planning Mode at Reliza.pdf
Pavel Shukhman
 
PPTX
Top iOS App Development Company in the USA for Innovative Apps
SynapseIndia
 
PDF
NewMind AI Journal - Weekly Chronicles - July'25 Week II
NewMind AI
 
PDF
Blockchain Transactions Explained For Everyone
CIFDAQ
 
PPTX
Darren Mills The Migration Modernization Balancing Act: Navigating Risks and...
AWS Chicago
 
PPTX
Top Managed Service Providers in Los Angeles
Captain IT
 
PPTX
Building and Operating a Private Cloud with CloudStack and LINBIT CloudStack ...
ShapeBlue
 
PDF
Building Resilience with Digital Twins : Lessons from Korea
SANGHEE SHIN
 
Rethinking Security Operations - SOC Evolution Journey.pdf
Haris Chughtai
 
✨Unleashing Collaboration: Salesforce Channels & Community Power in Patna!✨
SanjeetMishra29
 
The Builder’s Playbook - 2025 State of AI Report.pdf
jeroen339954
 
SFWelly Summer 25 Release Highlights July 2025
Anna Loughnan Colquhoun
 
Building a Production-Ready Barts Health Secure Data Environment Tooling, Acc...
Barts Health
 
Apache CloudStack 201: Let's Design & Build an IaaS Cloud
ShapeBlue
 
CloudStack GPU Integration - Rohit Yadav
ShapeBlue
 
TrustArc Webinar - Data Privacy Trends 2025: Mid-Year Insights & Program Stra...
TrustArc
 
CIFDAQ Token Spotlight for 9th July 2025
CIFDAQ
 
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
Wojciech Ciemski for Top Cyber News MAGAZINE. June 2025
Dr. Ludmila Morozova-Buss
 
LLMs.txt: Easily Control How AI Crawls Your Site
Keploy
 
Windsurf Meetup Ottawa 2025-07-12 - Planning Mode at Reliza.pdf
Pavel Shukhman
 
Top iOS App Development Company in the USA for Innovative Apps
SynapseIndia
 
NewMind AI Journal - Weekly Chronicles - July'25 Week II
NewMind AI
 
Blockchain Transactions Explained For Everyone
CIFDAQ
 
Darren Mills The Migration Modernization Balancing Act: Navigating Risks and...
AWS Chicago
 
Top Managed Service Providers in Los Angeles
Captain IT
 
Building and Operating a Private Cloud with CloudStack and LINBIT CloudStack ...
ShapeBlue
 
Building Resilience with Digital Twins : Lessons from Korea
SANGHEE SHIN
 
Ad

Structuring React.js Components

  • 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
  • 47. I thought GENERIC is a good thing
  • 51. 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)
  • 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 extends React.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’) /// class MemesPage 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 the king • project • stage of project • component • developer preferences • requirements
  • 60. “We value code that is easy to maintain 
 over code that is easy to write” Nat Pryce, Steve Freeman Bartek Witczak @bartekwitczak [email protected]