Building a Theming System with React

Building a Theming System with React

In my career I've created several component libraries, and always found it challenging and rewarding at the same time. The engine we developed at WorkWave is the result of such attempts: it offers a great tradeoff between flexibility and ease of use. Styling with inline styles greatly simplifies the developer's work while the use of Aphrodite to output real classes enables the full spectrum of css features. I'll show you the choices we made, what problems have arisen while developing the components and how we solved them, hopefully helping you make better choices for your own libraries.

5171121655bc1a7cc89d16a7a3ce8885?s=128

Matteo Ronchi

March 24, 2017
Tweet

Transcript

  1. BUILDING A THEMING SYSTEM WITH REACT

  2. MATTEO RONCHI @CEF62 GITHUB/CEF62 SENIOR SOFTWARE ENGINEER AT WORKWAVE FEVR

    COMMUNITY LEADER
  3. TODAY'S WEB IS ALL ABOUT COMPONENT-BASED ARCHITECTURES

  4. WHY COMPONENTS?

  5. > Modular > Scalable > Extensible > Reusable > Encapsulated

  6. COMPONENTIZATION ENABLES HIGHER ORDER COMPONENTS

  7. HOCS ALLOW ABSTRACTION OF REUSABLE AUGMENTATIONS

  8. MANY WAYS TO STYLE COMPONENTS

  9. PLAIN CSS

  10. Web pages load Cascading Style Sheets to learn how to

    render HTML elements .red-button { color: red; } <button class="red-button">Red</button>
  11. CSS-Modules

  12. Allow per-component css rules greatly improving components portability .redButton {

    color: red; } import css from './styles.css' const comp = () => <button className={css.redButton}>Red</button> <!-- HTML output --> <button class="red_button_7655465hgfhyoda4567">Red</button>
  13. Inline Styles

  14. Component styling without using CSS Making it really easy to

    dynamically change the visual output based on a component's state const redButton { color: 'red' } const comp = () => <button style={redButton}>Red</button>
  15. Runtime Generated CSS

  16. In recent years CSS-in-JS continued to grow and evolve. The

    current state of the art is expressed by libraries as > Styled Components > Aphrodite > Styletron > Glamour
  17. Those libraries dynamically define CSS rules using Javascript, JSON or

    CSS syntaxes. Generated CSS classes are injected at runtime into the web page.
  18. COMPONENTS THEMING

  19. In UI development theming is the definition of reusable sets

    of visual styles, aiming to create consistency across views
  20. UI Theming can be done in so many different ways

    mostly depending on per-project requirements
  21. COLOR PALETTES THE SIMPLEST WAY TO KEEP CONSISTENCY ACROSS VIEWS

    AND COMPONENTS
  22. @nice-blue: #5B83AD; @light-blue: #98C7F9; @small: 0.6rem; @big: 1.3rem; #header {

    color: @light-blue; font-size: @big; } #footer { color: @nice-blue; font-size: @small; }
  23. PROPS DRIVEN STYLES GREATER CUSTOMIZATION AND EASE TO CREATE DYNAMIC

    STYLES DEFINITION
  24. const theme = { niceBlue: '#5B83AD', lightBlue: '#98C7F9', small: '0.6rem',

    normal: '1rem', } const bgStyle = (props) => props.isFooter ? props.theme.lightBlue : props.theme.niceBlue const fontStyle = (props) => props.isFooter ? props.theme.small : props.theme.normal // styled-components const Button = styled.div` backgroundColor: ${bgStyle}; border: 2px solid ${fontStyle}; `
  25. Color Palettes and Props Driven Styles are great but we

    - at WorkWave - looked for something different
  26. > change the whole theme at runtime > support WYSIWYG

    > decouple component UI definition from UI styling > easy implementable theme variations
  27. > support theme variations per-component branches > be agnostic on

    how to create the final output > support component-based and atomic-css approaches
  28. WORKWAVE THEME-MANAGER

  29. The Theme-Manager is responsible for sharing themes It's possible to

    subscribe to be notified of theme changes, enabling a set of components to update consistently in a easy and transparent way
  30. A THEME DEFINES TWO MAIN FIELDS: styles a map of

    categories. Each category is a map of css classes in Javascript notation presets a map of presets, each preset is used to programmatically override the main theme definition
  31. const source = { // Main theme styles: { Button:

    { button: { backgroundColor: 'lightpink', color: 'blue', padding: '0.6em', fontWeight: 800, }, }, }, presets: { // dark style variation dark: { Button: { button: { backgroundColor: 'violet', color: 'white', }, }, }, }, } const appTheme = theme(source)
  32. const source = { // Main theme styles: { colors:

    { normal: { color: 'blue' }, confirm: { color: 'red' }, }, backgrounds: { normal: { backgroundColor: 'lightpink' }, confirm: { backgroundColor: 'lightgreen' }, }, }, presets: { // dark style variation dark: { colors: { normal: { color: 'white' }, confirm: { color: 'yellow' }, }, backgrounds: { normal: { backgroundColor: 'violet' }, confirm: { backgroundColor: 'darkgreen' }, }, }. }, } const appTheme = theme(source)
  33. A Themed Component expects to receive a map of css

    classes as a prop Knowing how to apply those css classes to its internal implementation.
  34. REACT BINDINGS

  35. <ThemeProvider />

  36. Makes available a theme-Manager instance to every descendant A theme-Manager

    instance can also be passed as a prop to every themed component
  37. connectTheme()

  38. A HoC connecting a React component to a themeManager instance

    connectTheme(config, mapThemeToProps, options)(DecoratedComponent)
  39. A themed component declares which theme categories it's interested in,

    and a map-Theme-To-Props function const config = { types: ['Header'] } const mapThemeToProps = ({ Header: { wrapper, label } }) => ({ wrapper, label }) const themedHeader = connectTheme(config, mapThemeToProps)(Header)
  40. The decorated component will receive a css prop containing all

    generated css classes const Header = ({ css, title }) => <div className={css.wrapper}> <div className={css.label}>{title}</div> </div>
  41. mapThemeToProps is where you are in control mapThemeToProps(theme, props) ->

    composed styles
  42. const mapThemeToProps = (theme) => { const { colors, backgrounds

    } = theme return { wrapper: { ...backgrounds.normal }, normalBtn: { ...colors.normal, ...backgrounds.normal }, confirmBtn: { ...colors.confirm, ...backgrounds.confirm }, } } const DecoratedComponent = ({ css }) => <div className={css.wrapper}> <button className={css.normalBtn}>Do Something</button> <button className={css.confirmBtn}>Are You Sure?</button> </div>
  43. const mapThemeToProps = (theme, props) => { const { colors,

    backgrounds, disabled } = theme const { canConfirm } = props const normalDisabled = canConfirm ? disabled.normal : {} const confirmDisabled = canConfirm ? disabled.normal : {} return { wrapper: { ...backgrounds.normal }, normalBtn: { ...colors.normal, ...backgrounds.normal, ...normalDisabled }, confirmBtn: { ...colors.confirm, ...backgrounds.confirm, ...confirmDisabled }, } } const DecoratedComponent = ({ css }) => <div className={css.wrapper}> <button className={css.normalBtn}>Do Something</button> <button className={css.confirmBtn}>Are You Sure?</button> </div>
  44. The decorated component's interaction with the theme is customizable using

    the theme-Params prop
  45. preset: a theme preset to be applied to the component

    branch. theme-Name: changes the active theme for the component branch. overrides: a local overrides to the style object passed to the map-Theme-To-Props.
  46. const themeParams = { preset: 'dark', overrides: { colors: {

    normal: { color: 'white', fontWeight: 'bold', }, }, backgrounds: { normal: { backgroundColor: 'black', }, }, } } const Comp = () => <MyThemedComp themeParams={themeParams} />
  47. PLUGIN BASED OUTPUT ENGINE

  48. The map of styles returned by map-Theme-To-Props is passed through

    a compiler We use aphrodite to generate CSS but the engine is configurable on per-component basis
  49. Performances Matters Every themed component uses memoization and strict equality

    checks to ensure the compiler is invoked as little as possible
  50. Room for improvements The code boilerplate required to create reusable

    variations of a themed component feels excessive Declaration of expected theme categories could be more flexible
  51. mronchi at workwave dot com

  52. THANKS! @CEF62