Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Patterns and Best Practices for developing extensible and high performance SPAs

Patterns and Best Practices for developing extensible and high performance SPAs

Patterns and Best Practices for developing extensible and high performance SPAs using the Neos CMS user interface as an example - JS Meetup Dresden

Sebastian Kurfürst

July 12, 2018
Tweet

More Decks by Sebastian Kurfürst

Other Decks in Technology

Transcript

  1. JS Meetup 12.07.2018 | Dresden Patterns and Best Practices for

    developing extensible and high performance SPAs using the Neos CMS user interface as an example
  2. tree-structured content main (ContentCollection) support (Page) SignUp (Form) KiteSupport (Text)

    sidebar (ContentCollection) FindSerialNumbers (Image) WinAKite (Text) completely customizable node types start with sane default types, or roll your own
  3. SideBar render() props App render() class App extends Component {

    render() { return <SideBar isHidden={/*todo*/} /> } } class SideBar extends Component { render() { return ( <div className={this.props.isHidden ? 'sidebar--hidden' : 'sidebar--visible'}> TODO: Render sidebar here... </div> ); } }
  4. class SideBar extends Component { render() { return ( <div

    className={this.props.isHidden ? 'sidebar--hidden' : 'sidebar--visible'}> TODO: Render sidebar here... </div> ); } } initial rendering and data update use same code path!
  5. class SideBar extends Component { shouldComponentUpdate(nextProps) { // somehow compare

    nextProps and this.props, // and return true if update is needed } }
  6. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false mutating state
  7. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false mutating state
  8. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false deep comparison needed! mutating state
  9. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state
  10. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar
  11. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar
  12. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar UI state
  13. state data UI sidebar collapsed = true ... ... ...

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false immutable state sidebar UI state
  14. state data UI sidebar collapsed = true ... ... ...

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false reference comparison is enough! immutable state sidebar UI state
  15. state data UI sidebar collapsed = true ... ... ...

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false reference comparison is enough! immutable state sidebar UI state !! great performance !!
  16. embrace PureComponent class SideBar extends PureComponent { // - component

    is like a pure function, // depending only on its input props. // - Automatically shallowly compares // props by reference (like we have // just seen) }
  17. *reselect 1 const contextForNodeLinking = createSelector( 2 [ 3 $get('currentlyEditedUserId'),

    4 $get('users') 5 ], 6 function( 7 currentlyEditedUserId, 8 users 9 ){ 10 return users[currentlyEditedUserId]; 11 } 12 );
  18. meet the Registries and manifest files can be queried for

    instances of components and application parts only point in lifecycle where registries can be mutated.
  19. time application bootstrap initialize manifest loader include extension scripts and

    register their manifests execute the manifests modify the registries as needed freeze registries continue with application bootstrap
  20. { "scripts": { "build": "neos-react-scripts build", "watch": "neos-react-scripts watch" },

    "devDependencies": { "@neos-project/neos-ui-extensibility": "~1.0.8" }, "neos": { "buildTargetDirectory": "../../Public/ColorPickerEditor" }, "dependencies": { "react-color": "^2.11.1" } } package.json
  21. import manifest from '@neos-project/neos-ui-extensibility'; import ColorPickerEditor from './ColorPickerEditor'; manifest('Neos.Neos.Ui.ExtensibilityExamples:ColorPickerEditor', {},

    globalRegistry => { const inspectorRegistry = globalRegistry.get('inspector'); const editorsRegistry = inspectorRegistry.get('editors'); editorsRegistry.set('Neos.Neos.Ui.ExtensibilityExamples/ColorPickerEditor', { component: ColorPickerEditor }); }); manifest.js
  22. export default class ColorPickerEditor extends Component { handleChangeColor = newColor

    => { this.props.commit(newColor.hex); }; render() { return (<div> <SketchPicker color={this.props.value} onChange={this.handleChangeColor}/> </div>); } } ColorPickerEditor.js
  23. import manifest from '@neos-project/neos-ui-extensibility'; import wrapWithKeyboardListener from './WrapWithKeyboardListener'; manifest('Neos.Neos.Ui.ExtensibilityExamples:WrapWithKeyboardListener', {},

    globalRegistry => { const containerRegistry = globalRegistry.get('containers'); const ApplicationContainer = containerRegistry.get('App'); const enhancedApplicationContainer = wrapWithKeyboardListener(ApplicationContainer); containerRegistry.set('App', enhancedApplicationContainer); }); manifest.js
  24. //... const wrapWithKeyboardListener = OriginalApp => { return class CustomKeyboardListener

    extends Component { //... render() { return ( <React.Fragment> {this.renderPalette()} <OriginalApp {...this.props}/> </React.Fragment> ); } //... } } export default wrapWithKeyboardListener; WrapWithKeyboardListener
  25. @neos(globalRegistry => ({ containerRegistry: globalRegistry.get('containers') })) export default class LeftSideBar

    extends PureComponent { render() { const {containerRegistry} = this.props; const LeftSideBarTop = containerRegistry.getChildren('LeftSideBar/Top'); return ( <SideBar> <div className={style.leftSideBar__top}> {LeftSideBarTop.map((Item, key) => <Item key={key}/>)} </div> </SideBar> ); } }
  26. •handles •after handles •10 •20 •before handles •end •start •start

    100 •end 100 Implemented in @neos-project/positional-array-sorter
  27. react Neos UI App Color Picker Plugin core JS bundle

    dynamically linked architecture react shim ... plugin b.
  28. window.NeosApiExposureMap = { '@vendor': { React, ReactDOM, PropTypes // ...

    }, '@NeosProjectPackages': { NeosUiEditors, ReactUiComponents // ... } }; export public APIs
  29. webpack config return { // ... resolve: { // override

    config! alias: { 'react': '@neos-project/neos-ui-extensibility/src/shims/vendor/react/index', 'react-dom': '@neos-project/neos-ui-extensibility/src/shims/vendor/react-dom/index', 'prop-types': '@neos-project/neos-ui-extensibility/src/shims/vendor/prop-types/index', '@neos-project/react-ui-components': '@neos-project/neos-ui-extensibility/src/shims/neosProjectPackages/react-ui-components/index', '@neos-project/neos-ui-editors': '@neos-project/neos-ui-extensibility/src/shims/neosProjectPackages/neos-ui-editors/index', } } }; module.exports = window.NeosApiExposureMap['vendor'].React; shim file