Redux Internals
Vladimir Novick
mail: vnovick@gmail.com
twitter: @VladimirNovick
github: vnovick
facebook: vnovickdev
Frontend Developer & Architect
Redux
Agenda
-
Redux in a nutshell
-
Context wormholes
-
Middleware signature
-
Enhancing store
Redux in a nutshell
Is Flux looks like this?
or...
Well Redux is
So Why Redux?
-
Separation of concerns
-
FP driven
-
Awesome developer experience
Three Principles
-
Single source of truth
-
State is changed only by emitting an action
-
Mutations described by pure functions (reducers)
The data flow
Creating a store
import { createStore, combineReducers, } from 'redux';
import app from './appReducer';
import user from './userReducer';
const rootReducer = combineReducers({
app,
user
});
const store = createStore(rootReducer)
Basic reducer
import { FANVOICE_ACTION_TYPES } from './fanvoiceActions.js';
const INITIAL_STATE = {
IS_INITIALIZED: false
};
function setState(state, newState){
return { ...state, ...newState };
}
export function fanvoiceReducer(state = INITIAL_STATE, action){
if (FANVOICE_ACTION_TYPES) {
switch (action.type) {
case FANVOICE_ACTION_TYPES.FETCH.SUCCESS:
case FANVOICE_ACTION_TYPES.FETCH.FAILED:
return setState(state, action.state);
default:
return state;
}
}
return state
}
Action Creator
function displayPopup(shouldShowPopup){
return {
type: TOGGLE_POPUP,
state: {
shouldShowPopup
}
};
}
Thunk Action Creator
export const getVideos = (api, userAuth) => (dispatch, getState) =>{
const state = getState();
const { apiKey, apiUrl } = state.app.config.api;
const { accessToken, id } = state.user.auth;
const url = `${apiUrl}/filter/videos/sort/user_id=${id}`,
dispatch({
type: '@@fetcher/FETCH',
state: {
url,
method,
resolve: result => ({
type: 'FETCH_SUCCESS',
state: result
}),
reject: err => ({
type: 'FETCH_ERROR',
state: err
}),
error: err => ({
type: 'RESPONSE_ERROR',
state: err,
transform: () => dispatch({ type: 'CUSTOM_ERROR_ACTION' })
})
}
});
};
Container(Smart) Component:
Specifies data and behavior for component but does not know anything about appearance
Presentational(Dumb) Component:
Specify appearance of component but knows nothing about behavior
React Components
Dumb Component
const TodoApp = ({ store }) => (
<div>
<AddTodo store={store} />
<VisibleTodoList store={store} />
<Footer store={store} />
</div>
);
Container Component
function getVisibleTodos(){
//...
}
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { getState, dispatch } = this.props.store;
const { todos, visibilityFilter } = store.getState();
return (
<TodoList
todos={ getVisibleTodos( todos, visibilityFilter ) }
onTodoClick={id => dispatch({ type: 'TOGGLE_TODO', id }) }
/>
);
}
}
Passing Store techniques
Passing explicitly via props
The problem:
If you have a component tree, store must be passed through props for all components in the tree. It requires additional glue of subscribing, unsubscribing, getting state e.t.c
Context wormholes
Passing Store techniques
The solution:
Only container components get store from context when defining contextTypes.
Passing implicitly via Context
<Provider store={store}>
Context
connect(...fns)(component)
React Redux
Provider
import { Provider } from 'react-redux'
import { appStore} from 'appStore'
<Provider store={ appStore }>
<PlayerContainer/>
</Provider>
connect(...fns)(component)
import * from 'actionCreators';
import { connect } from 'react-redux';
// ...
// ...Player React Component definition
function mapStateToProps(state){
const { src, poster, showControls } = state.player;
return {
src, poster, showControls
}
}
export const PlayerContainer = connect(
mapStateToProps
)(Player);
Passing around actions
import React from 'react';
import * from 'actionCreators';
import { connect } from 'react-redux';
class Player extends React.Component {
play(){
this.props.dispatch(actionCreators.play())
}
render() {
return (
<PlayerWrapper>
<PlayerSurface/>
<PlayerControls play={this.play.bind(this}/>
</PlayerWrapper>
)
}
}
//export PlayerContainer with connect as in previous slide
mapDispatchToProps
import React from 'react';
import * from 'actionCreators';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
class Player extends React.Component {
render() {
return (
<PlayerWrapper>
<PlayerSurface/>
<PlayerControls play={this.props.actions.play} />
</PlayerWrapper>
)
}
}
function mapStateToProps(state){
//...same as previous slide
}
function mapDispatchToProps(dispatch){
return {
actions: bindActionCreators(actionCreators, dispatch)
}
}
export const PlayerContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Player);
Middleware signatures
Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s dispatch method for fun and profit
Middleware is a dispatch wrapper
import createLogger from 'redux-logger'
import { createStore } from 'redux'
let store = createStore(rootReducer,
applyMiddleware(thunk, createLogger())
)
Logger
Signature
store => next => action
Middleware
const applyStateTransform = (action) => {
let { transform, state } = action;
return transform ? transform(state) || state : state
}
const fetcher = store => next => action => {
if (action.type === '@@fetcher/FETCH'){
let { url, method, resolve, reject, error } = action.state;
fetch(url, {
method
}).then((result)=> {
let resolveAction = resolve(result);
let errorAction = error(result);
return result.ok ?
next({
type: `@@fetcher/${resolveAction.type}`,
state: applyStateTransform(resolveAction)
}) :
next({
type: `@@fetcher/${errorAction.type}`,
state: applyStateTransform(errorAction)
})
}).catch((err) => {
let rejectAction = reject(err);
return next({
type: `@@fetcher/${rejectAction.type}`,
state: applyStateTransform(rejectAction)
})
});
}
return next(action)
}
////Fetch Action Creator
export const fetchFromServer = (url, method) => dispatch => {
dispatch({
type: '@@fetcher/FETCH',
state: {
url,
method,
resolve: result => {
return {
type: 'FETCH_SUCCESS',
state: result
}
},
reject: err => {
return {
type: 'FETCH_ERROR',
state: err
}
},
error: err => {
return {
type: 'RESPONSE_ERROR',
state: err,
transform: ()=>{
dispatch({
type: 'Lalala'
})
}
}
}
}
})
}
Custom fetch middleware
Enhancing a store
Signature
createStore => createStore
// Usage
componentDidMount(){
let { dispatch } = this.props;
dispatch(inject('player', this))
console.log(appStore.getState(this))
}
//Result will be player stateKey value
//Inject Action Creator
export const inject = (state, component) => {
return {
type: '@@redux/INJECT',
state
component
}
}
function diStoreEnhancer(){
return createStore => (...args) => {
const store = createStore(...args);
const diContainer = []
const dispatch = function decoratedDispatch(action){
if (action.type === '@@redux/INJECT') {
const { component, state } = action;
diContainer.push({ component, stateKey: state })
}
return store.dispatch(action)
}
const getState = function decoratedState() {
if (component) {
return store.getState()[diContainer
.filter(di => di.component === component)
.reduce( (prev,next) => next, {})
.stateKey
];
}
return store.getState()
}
return {
...store,
...{
dispatch,
getState
}
};
};
}
Compose
import { compose } from 'redux'
const createComposedStore = compose(
storeEnhancer(),
applyMiddleware(fetcher, createLogger()
)(createStore);
export default createComposedStore(rootReducer);
React-Redux summary
-
Provider: wraps root component and makes possible to use connect()
-
connect: executed on every state change if specified. returned object is merged into component props
- mapStateToProps: executed on every state change if specified. returned object is merged into component props
- mapDispatchToProps: executed on every state change, given dispatch as parameter. returned object will be injected into props. Mostly used together with bindActionCreators
react@90min.com
Redux Internals (JS -Israel)
By vladimirnovick
Redux Internals (JS -Israel)
Javascript Israel Meetup
- 1,980