Component Composition

L&L April 2022
Steve Olsen

GOALS:

  • To learn something new
  • Smile twice (or more)

Ways to shave

Pros and cons

  • No "one right" answer!
  • Tradeoffs abound
  • Implementation details mater, too!

# The problem

What's all the ado?

Code reuse is hard

  • DRY
  • Inheritance
  • Libraries
  • Gists
  • Functions
  • Loops
  • etc.

# Functions

Code that processes data

What is a function?

A series of statements

that are common to solving a problem

PURE function:

  • Single purpose ideally

    • (rule of thumb: 10 statements, early exit)

  • No external state or data dependencies

    • (i.e. no closures, i/o)

  • REPEATABLE / idempotent

  • Highly refactorable

  • Highly testable

  • Highly reusable

SIDE-EFFECT function:

  • Single purpose ideally

    • (rule of thumb: 10 statements, early exit)

  • HAS external state or data dependencies

    • (i.e. closures, i/o)

  • CHANGE TO STATE

  • NON-REPEATABLE / non-idempotent

  • HARDer to refactor

  • HARDer to test

  • HARDer to reuse

SIDE-EFFECT function:

PURE function:

export const add = (a, b) => a + b
import fs from "fs"

export const saveToFile = async (filename, text) => {
  return fs.writeFile(filename, text)
}

export const readFile = async (filename) => {
  return fs.readFile(filename)
}

export const callApi = async (id) => {
  return fetch(`http://123.com/${id}`).then(r => r.json())
}

let id = 0
export const getId() {
  return id++
}

Date.now() === '?????'

Example Call Stack

main()

Tell Me More.

# Pattern

"composition"

OOP: Composition over inheritance

contain instances, instead of
inheriting from a parent

FP: "Onion Layers"

wrappers that intercept an
inner function's execution

Each "onion layer" is a function

Official React Design Patterns

Composition is #1!

"An {X} that returns an {X}"

  1. HOF: A function that returns a function

  2. HOC: A Component that returns a Component

"Higher Order {X}"

const g = f(x)
const G = <F><X/></F>

Practical Examples:

// Constructor ///////////

const handleButton = (index) => () => {
  console.log("Clicked button #" + index)
}
...
{[1,2,3].map((number, idx) =>
    <button onClick={handleButton(idx)}>{number}</button>)}

// Decorator ///////////

const timeFunc = (cb) => () => {
  const now = Date.now()
  cb()
  console.log("TIME LAPSED (ms):", Date.now() - now)
}

const looper = (times) {
  for (let i = 0; i < times; i++) {
    console.log("i=" + i)
  }
}

const timedLoop = timeFunc(() => looper(99999))
timedLoop() // TIME LAPSED (ms): 4
timedLoop() // TIME LAPSED (ms): 6
timedLoop() // TIME LAPSED (ms): 3

Curry:

// Theory

const add = (a, b) => a + b

const addBy5 = add(5)

addBy5(5) === 10

A function that returns a function

until all its arguments are provided.

JavaScript does not provide this natively,
but there are wrappers to provide it!

Wrapper Pattern examples

  • Facade
  • Adapter
  • Middleware
  • Interceptor
  • Decorator
Eg. [ [ pres ] state({initial}) ]
Eg. [ [ apiGetUser ] debounce({seconds}) ]

Bonus pattern: reducers

PURE / observer / event bus

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'cnt/increment':
      return {count: state.count + 1};
    case 'cnt/decrement':
      return {count: state.count - 1};
    // default:
    //  throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'cnt/decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'cnt/increment'})}>+</button>
    </>
  );
}

# Code Quality

aka. low ratio of wtf-per-minute

Code Quality Phases

OPTIMIZED (5%)

MAINTAINABLE (90%+)

WORKING (5%)

refactor, test

heavy test

Misc Notes:

  1. Refactoring (breaking apart, polishing) is critical!
  2. All essential code must go somewhere —

    in another well-named file (follow naming conventions)

    • split up per responsibility

    • benefits:

      • less scrolling/hopping around file ("fits in your head")

      • fewer chances for git merge conflicts
      • separate config/types/consts separately from implementation module for 3rd party importing
  3. VS Code: set "Window Title":
    • ${dirty} ${activeEditorMedium}${separator}${rootName}

Coupling

  • "Connascence": level of coupling

  • Dimensions: strength, degree, locality

  • Not all coupling is "bad"

  • Tradeoffs made by the developer

    • important to understand them!

# Components

they're highly functional

Separation of concerns

  • data presentation

  • event handling

  • state manipulation

  • Data validation [assert]

  • Data manipulation [map/reduce]

  • Data API calls [I/O]

  • Analytics / metics / logging [monitoring I/O]

Separation of concerns

  • data presentation

  • event handling

  • state manipulation

  • Data validation [assert]

  • Data manipulation [map/reduce]

  • Data API calls [I/O]

  • Analytics / metics / logging [monitoring I/O]

Separation of concerns

  • data presentation

  • event handling

  • state manipulation

  • Data validation [assert]

  • Data manipulation [map/reduce]

  • Data API calls [I/O]

  • Analytics / metics / logging [monitoring I/O]

Pres

Contain

Func/

BusLog

Separation of concerns

  • data presentation
  • event handling
  • inner state manipulation

a1) Presentation components:

  • app state manipulation

  • wire up business logic to presentation component

a2) Container components:

  • data API calls [I/O]
  • data validation [assert]
  • data manipulation [map/reduce]

  • analytics / metics / logging [monitoring I/O]

b) Business Logic functions:

Utitlity Components

  • Goal: reduce copy/paste, DRY!

  • Create new component APIs that encapsulate common patterns and conventions

  • Often a parent/child relationship

  • Common ones are shared:

    • maximize reuse

    • minimize redundancy

  • Think about this like “a bucket of specific Figma assets” ala drag-and-drop reuse

    • Eg. Carousel, Select box, Navigation

Tips and resources

  • Other names for this component pattern:
    • “smart/dumb”, “widget/presentation”, “effect/pure”
  • Check the imports! This will tell you a lot about what the component is doing. (Maybe it's too much?)
  • Refactoring normally creates more files, not fewer
    • Naming files well is crucial for maintenance!
  • Patern links:
    • https://www.patterns.dev/posts/presentational-container-pattern/
    • https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
    • https://gist.github.com/chantastic/fc9e3853464dffdb1e3c

Standard non-component files:

  • index.ts : “public API” to be imported by other components
  • styles.ts : CSS
  • types.ts : interfaces, types
  • config.ts : enums, etc.
    • Example: urlBase, userTypes
  • state.ts or reducer.ts *** TBD ***

# Hands-on

Live Example

Review Snap components

front_end/src/views-mui/header/header-omni/header-omni-mobile.component.js link
  • mixed together presentation, state, event handling
  • separate components, new files
front_end/src/components-mui/header-desktop/header-desktop-nav.container.js link
  • component duplication, event handling, io

  • utility components, new files, decouple BL

snapcommerce-spa/src/routers/auth-required.tsx link
  • multiple components, mixed purposes, no testing (because…. how?)
  • sub-components, new files - FIXED!!

# THANK YOU

Questions?

"Good architecture has big benefits, but it needs: education, discipline, and enforcement."

Made with Slides.com