Files
S.P.L.U.R.T-Station-13/tgui/packages/common/redux.js
2020-09-02 17:52:47 +03:00

154 lines
4.0 KiB
JavaScript

/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { compose } from './fp';
/**
* Creates a Redux store.
*/
export const createStore = (reducer, enhancer) => {
// Apply a store enhancer (applyMiddleware is one of them).
if (enhancer) {
return enhancer(createStore)(reducer);
}
let currentState;
let listeners = [];
const getState = () => currentState;
const subscribe = listener => {
listeners.push(listener);
};
const dispatch = action => {
currentState = reducer(currentState, action);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
};
// This creates the initial store by causing each reducer to be called
// with an undefined state
dispatch({
type: '@@INIT',
});
return {
dispatch,
subscribe,
getState,
};
};
/**
* Creates a store enhancer which applies middleware to all dispatched
* actions.
*/
export const applyMiddleware = (...middlewares) => {
return createStore => (reducer, ...args) => {
const store = createStore(reducer, ...args);
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed.');
};
const storeApi = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
const chain = middlewares.map(middleware => middleware(storeApi));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
};
/**
* Combines reducers by running them in their own object namespaces as
* defined in reducersObj paramter.
*
* Main difference from redux/combineReducers is that it preserves keys
* in the state that are not present in the reducers object. This function
* is also more flexible than the redux counterpart.
*/
export const combineReducers = reducersObj => {
const keys = Object.keys(reducersObj);
let hasChanged = false;
return (prevState = {}, action) => {
const nextState = { ...prevState };
for (let key of keys) {
const reducer = reducersObj[key];
const prevDomainState = prevState[key];
const nextDomainState = reducer(prevDomainState, action);
if (prevDomainState !== nextDomainState) {
hasChanged = true;
nextState[key] = nextDomainState;
}
}
return hasChanged
? nextState
: prevState;
};
};
/**
* A utility function to create an action creator for the given action
* type string. The action creator accepts a single argument, which will
* be included in the action object as a field called payload. The action
* creator function will also have its toString() overriden so that it
* returns the action type, allowing it to be used in reducer logic that
* is looking for that action type.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments
* and returns { payload } or { payload, meta }. If this is given, the
* resulting action creator will pass it's arguments to this method to
* calculate payload & meta.
*
* @public
*/
export const createAction = (type, prepare) => {
const actionCreator = (...args) => {
if (!prepare) {
return { type, payload: args[0] };
}
const prepared = prepare(...args);
if (!prepared) {
throw new Error('prepare function did not return an object');
}
const action = { type };
if ('payload' in prepared) {
action.payload = prepared.payload;
}
if ('meta' in prepared) {
action.meta = prepared.meta;
}
return action;
};
actionCreator.toString = () => '' + type;
actionCreator.type = type;
actionCreator.match = action => action.type === type;
return actionCreator;
};
// Implementation specific
// --------------------------------------------------------
export const useDispatch = context => {
return context.store.dispatch;
};
export const useSelector = (context, selector) => {
return selector(context.store.getState());
};