Redux Beyond SPA

To Infinity and beyond

Vladimir Novick

Frontend Developer & Architect

mail: vnovick@gmail.com

twitter: @VladimirNovick

github: vnovick

facebook: vnovickdev

How we used Redux with Server Side rendering in Rails

SPA

SSR

?

SPA and SSR are on the same spectrum

we are here

+

Why we don't use SPA

  1. Time to first paint is longer
  2. Slow performance on low end devices
  3. Huge bundle.js
  4. Proper SEO harder to implement
  5. Loading indicator all over the place
  6. Increased chance to memory leaks because of page longlivity
  7. Disabled JS will lead to blank plage

What if you want to render a page in Rails and let React & Redux to take over several components on the page 

or

You already have server side rendering

Lack of examples

So if you have Rails/.NET/Java/Django server rendering should you start from scratch?

Isomorphic 

Universal Apps

Not Always

let node = document.querySelector.bind(document);

const Video = ({src}) => {
  return <iframe src={src}/>
}

ReactDOM.render(
    <Video src="https://www.youtube.com/embed/u3uh46HiWJ8"/>,
    node('.container__mountPoint1')
)

ReactDOM.render(
    <Video src="https://www.youtube.com/embed/u3uh46HiWJ8"/>,
    node('.container__mountPoint2')
)

Let's make our code DRY

const Video = ({src}) => {
  return <iframe src={src}/>
}

const componentsToRender = [{
  mountPoint: 'container__mountPoint1',
  component: Video,
  props: {
    src: "https://www.youtube.com/embed/u3uh46HiWJ8"
  }
},{
  mountPoint: 'container__mountPoint2',
  component: Video,
  props: {
    src: "https://www.youtube.com/embed/u3uh46HiWJ8"
  }
}]

const reactMount = (Component, selector, props) => {
  const mountPoint = node(`.${selector}`);
  if (mountPoint){
    ReactDOM.render(<Component {...props}/>,    mountPoint)
  }
}

componentsToRender.forEach(({ mountPoint, component, props }) => 
    reactMount(component, mountPoint, props))

What are we missing

multiple runtime dependent mountable components

State cannot be shared between components

 

To manage application state

Redux Benefits

1. Functional Programming approach

2. Components can share state even when they are rendered in different mount points

3. Redux is non opinionated and very customizable and extensible

How to use with Redux

ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);
ReacDOM.render(
    <Provider store={appStore}>
      <Component/>
    </Provider>, 
    document.querySelector('.mount-point')
);

Not DRY

&&

Hard to maintain

===

Hard to scale

Let's Configure Redux

import { Provider } from 'react-redux';
import { 
    compose,
    createStore,
    applyMiddleware,
    combineReducers
} from 'redux';

const APP_REDUCER_INITIAL_STATE = {};


function appReducer(state = APP_REDUCER_INITIAL_STATE, action){
  switch (action.type) {
    default:
  }
  return state;
}

const rootReducer = combineReducers({ appReducer })
const appStore = compose(applyMiddleware(/* My Middlewares */))
    (createStore)(rootReducer);

reduxMount

const reduxMount = (Component, selector, props) => {
  const mountPoint = node(`.${selector}`);
    ReactDOM.render(
      <Provider store={appStore}>
        <Component {...props}/>
      </Provider>,    mountPoint)
}

Still not maintanble

  • rootReducer should be composed of all reducers in the application
  • Several places to update when new component added

Make components declare their dependencies

The Plan

  1. New component scaffold
    • Component should export the following scaffold
      • component function
      • component name
      • stateKey
      • reducer
    • reduxMount should be modified to deal with new component structure
  2. reduxMount should addReducer in runtime with corresponding stateKey
  3. reducers should be configured in app state

Step 1

Container Component scaffold

export const VideoContainer = {
  component: connect()(Video),
  name: 'VideoContainer',
  stateKey: 'video',
  reducer: videoReducer
}
const rootReducer = combineReducers({
 appReducer, 
 videoReducer 
})
const reduxMount = (container, selector, props) => {
  const mountPoint = node(`.${selector}`);
  const { component: Component } = container;
    ReactDOM.render(
      <Provider store={appStore}>
        <Component {...props}/>
      </Provider>,    mountPoint)
}

The Plan

  1. New component scaffold
    • Component Should export the following scaffold
      • component function
      • component name
      • stateKey
      • reducer
    • reduxMount should be modified to deal with new component structure
  2. reduxMount should addReducer in runtime with corresponding stateKey
  3. reducers should be configured in app state

Step 2 replaceReducer and store reconfig

let reducers = {
  app: appReducer
}

function addReducer(stateKey, reducer){
  reducers = { ...reducers, [stateKey]: reducer }
  return appStore.replaceReducer(
    combineReducers({ reducers })
  )
}

const rootReducer = combineReducers(reducers)
const reduxMount = (container, selector, props) => {
  const mountPoint = node(`.${selector}`);
  const { component: Component, stateKey, reducer } = container;
    addReducer(stateKey, reducer)
    ReactDOM.render(
      <Provider store={appStore}>
        <Component {...props}/>
      </Provider>,    mountPoint)
}

The Plan

  1. New component scaffold
    • Component Should export the following scaffold
      • component function
      • component name
      • stateKey
      • reducer
    • reduxMount should be modified to deal with new component structure
  2. reduxMount should addReducer in runtime with corresponding stateKey
  3. reducers should be configured in app state

Step 3

Reducers inside application state

const APP_REDUCER_INITIAL_STATE = {
  reducers: {
    app: appReducer
  }
};
const appActions = {
  REGISTER_REDUCER: 'REGISTER_REDUCER'
}
function appReducer(state = APP_REDUCER_INITIAL_STATE, action){
  switch (action.type) {
    case appActions.REGISTER_REDUCER:
      return registerReducer(state, action.state);
    default:
  }
  return state;
}
function registerReducer(state, newState){
  const newReducersList = {
    ...state.reducers,
    ...newState
  }
  return {
    ...state,
    ...{
      reducers: newReducersList
    }
  }
}
function addReducer(stateKey, reducer){
  const newReducerState = {
     [stateKey]: reducer
  }
  appStore.dispatch({
    type: appActions.REGISTER_REDUCER,
    state: newReducerState
  })
  return appStore.replaceReducer(
    combineReducers({ 
      ...appStore.getState().app.reducers, 
      ...newReducerState  
    })
  )
}

The Plan

  • Component Should export the following scaffold
    • component function
    • component name
    • stateKey
    • reducer
  • reduxMount should be modified to deal with new component structure
  • reduxMount should addReducer in runtime with corresponding stateKey
  • reducers should be configured in app state

What else can be done in the same way

  • Additional logic on component mount
  • Conditional mounting
  • Redux Sagas
  • And actually any component dependency

Mount Logic Example

export const VideoContainer = {
  component: connect()(Test),
  name: 'VideoContainer',
  shouldMountPredicate: (state)=>{
    return true
  },
  stateKey: 'video',
  reducer: videoReducer
}
const reduxMount = (container, selector, props) => {
  const mountPoint = node(`.${selector}`);
  const { 
    component: Component, 
    stateKey, 
    reducer, 
    shouldMountPredicate 
  } = container;
  if (shouldMountPredicate(appStore.getState())){
     addReducer(stateKey, reducer)
     ReactDOM.render(
       <Provider store={appStore}>
          <Component {...props}/>
       </Provider>,    mountPoint
    ) 
  }
}

react@90min.com