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
- Time to first paint is longer
- Slow performance on low end devices
- Huge bundle.js
- Proper SEO harder to implement
- Loading indicator all over the place
- Increased chance to memory leaks because of page longlivity
- 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
- 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
-
Component should export the following scaffold
- reduxMount should addReducer in runtime with corresponding stateKey
- 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
-
New component scaffold-
Component Should export the following scaffoldcomponent functioncomponent namestateKeyreducer
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
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
-
New component scaffold-
Component Should export the following scaffoldcomponent functioncomponent namestateKeyreducer
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
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 scaffoldcomponent functioncomponent namestateKeyreducer
reduxMount should be modified to deal with new component structurereduxMount should addReducer in runtime with corresponding stateKeyreducers 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
Redux Beyond SPA
By vladimirnovick
Redux Beyond SPA
- 1,947