Internals
Constructor
- Use closure to store state and subscribe.
- Use middleware to change normal dispatch function.
function applyMiddleware(...middlewares) {
return (store) => {
// should return (next) => (action) => { ... } function
if (middlewares.length === 0)
return dispatch => dispatch
if (middlewares.length === 1)
return middlewares[0]
// [ (next) => (action) => {...}, ... ] array
// next: (action) => { ... } function
const boundMiddlewares = middlewares.map(middleware => middleware(store))
return boundMiddlewares.reduce((a, b) => next => a(b(next)))
}
}
function createStore(reducer, middleware) {
// closure for storing global state
let state
const subscribers = []
const coreDispatch = (action) => {
validateAction(action)
state = reducer(state, action)
subscribers.forEach(handler => handler())
}
const getState = () => state
const store = {
dispatch: coreDispatch,
getState,
subscribe: (handler) => {
subscribers.push(handler)
// unsubscribe function
return () => {
const index = subscribers.indexOf(handler)
if (index > 0)
subscribers.splice(index, 1)
}
},
}
if (middleware) {
// store default dispatch
const dispatch = action => store.dispatch(action)
// middleware = ({ dispatch, getState }) => (next) => (action) => { ... };
// middleware is a higher-order function (return (action) => { ... });
// dispatch, getState and coreDispatch are injected into middleware as arguments
store.dispatch = middleware({
dispatch,
getState,
})(coreDispatch)
}
coreDispatch({
type: INIT_REDUX,
})
return store
}
Action Validation
function isValidKey(key) {
return ['type', 'payload', 'error', 'meta'].includes(key)
}
function validateAction(action) {
if (!action || typeof action !== 'object' || Array.isArray(action))
throw new Error('Action must be an object!')
if (typeof action.type === 'undefined')
throw new TypeError('Action must have a type!')
if (!Object.keys(action).every(isValidKey)) {
throw new Error(
'Action only have `type`, `payload`, `error` or `meta` field!'
)
}
}
Provider
- use Context to provide store (two methods):
- inject store into every children recursively
- use Consumer in Connect higher order component
<Consumer>{store => (<WrapperComponent store={store}>)}</Consumer>
interface Store {
getState: Function
subscribe: Function
dispatch: Function
}
const StoreContext = React.createContext(store)
export function Provider({
store,
children,
}: {
store: Store
children: ReactElement
}) {
return (
<StoreContext value={store}>
<StoreContext.Consumer>
{(store) => {
const childrenWithStore = React.Children.map(children, child =>
React.cloneElement(child, { store }))
return <div>{childrenWithStore}</div>
}}
</StoreContext.Consumer>
</StoreContext>
)
}
function connect(
mapStateToProps = () => ({}),
mapDispatchToProps = () => ({})
) {
return (Component) => {
class Connected extends React.Component<{ store: Store }> {
onStoreOrPropsChange(props) {
const { store } = this.props
const state = store.getState()
const stateProps = mapStateToProps(state, props)
const dispatchProps = mapDispatchToProps(store.dispatch, props)
this.setState({
...stateProps,
...dispatchProps,
})
}
UNSAFE_componentWillMount() {
const { store } = this.props
this.onStoreOrPropsChange(this.props)
this.unsubscribe = store.subscribe(() =>
this.onStoreOrPropsChange(this.props)
)
}
UNSAFE_componentWillReceiveProps(nextProps) {
this.onStoreOrPropsChange(nextProps)
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
return <Component {...this.props} {...this.state} />
}
}
return Connected
}
}