undox
undox copied to clipboard
feature request: limit, filter, preserve
First of all, thanks for your work, it helped us a lot. We start using undox in prod.
Features I am missing:
- limit - some reducers are tricky and take a little time to execute. Undo takes almost 60ms for a 200-action history. The user can perform 20 actions per minute.
- filter - I don't want to include some actions (for example, open / close a menu) in the history
- preserve - for example FETCH_SUCCESS action must be in history to replay the history correctly. But I don't want to let the user undo the FETCH_SUCCESS action.
As a work around i wrap the undox reducer. Let me know if you open to Pull requests
import { UndoxTypes, Action, Reducer, UndoxState } from 'undox';
import _flow from 'lodash/flow';
function getHistoryAction<T, S extends UndoxState<T, A>, A extends Action>({
state,
offset,
}: {
state: S;
offset: number;
}) {
return state.history[state.index + offset];
}
type PreserveConfig = { preserve?: (action: Action | Action[]) => boolean };
const undoxEnhancerPreserve = <T, S extends UndoxState<T, A>, A extends Action>({
preserve = () => false,
}: PreserveConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
if (
state &&
action.type === UndoxTypes.UNDO &&
preserve(getHistoryAction({ state, offset: 0 }))
) {
return state;
}
return reducer(state, action);
};
type FilterConfig = { filter?: (action: Action) => boolean };
const undoxEnhancerFilter = <T, S extends UndoxState<T, A>, A extends Action>({
filter = () => true,
}: FilterConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
if (!state || Object.values(UndoxTypes).includes(action.type) || filter(action)) {
return reducer(state, action);
}
const newState = reducer(state, action);
const oldUndoxState = { index: state.index, history: state.history };
return {
...newState,
...oldUndoxState,
};
};
type LimitConfig = { limit?: { maxCount: number; resetStateAction: string } };
const undoxEnhancerLimit = <T, S extends UndoxState<T, A>, A extends Action>({
limit,
}: LimitConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
const newState = reducer(state, action);
if (limit && newState.history.length > limit.maxCount) {
const limitedActionsHistoryLength = Math.floor(limit.maxCount / 2);
const removedActionsHistory = newState.history.slice(
0,
newState.history.length - limitedActionsHistoryLength,
);
const removedActionsHistoryState = (removedActionsHistory.flat() as A[]).reduce(
reducer,
undefined,
);
const preservedActionsHistory = newState.history.slice(
newState.history.length - limitedActionsHistoryLength,
);
const newHistory = [
{ type: limit.resetStateAction, payload: removedActionsHistoryState?.present },
...preservedActionsHistory,
];
return {
...newState,
history: newHistory,
index: newHistory.length - 1,
};
}
return newState;
};
export const undoxEnhancer = <S, A extends Action>(
reducer: Reducer<S, A>,
config: PreserveConfig & FilterConfig & LimitConfig = {},
) => {
return _flow(
undoxEnhancerPreserve(config),
undoxEnhancerFilter(config),
undoxEnhancerLimit(config),
)(reducer);
};
const reducer = undoxEnhancer(undox(counterReducer, init()), {
limit: { maxCount: 6, resetStateAction: actionTypes.REINIT },
filter: ({ type }) => HISTORICAL_ACTIONS_TYPES.has(type),
preserve: ({ type }) => CAN_NOT_UNDO_ACTIONS_TYPES.has(type),
});