-
-
Notifications
You must be signed in to change notification settings - Fork 188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possibility to have flat redux structure #179
Comments
See #154 for some discussion on this. While it's (somewhat) easy to implement, name collisions are the main issue we want to avoid, so if you can think of a way to handle name collisions I'm all ears |
hi @davidroeca , thanks for the quick answer. I see that I'm not the first facing this problem.
@ugo-buonadonna I see that you make a fork with flat structure. Did you face any problems caused by such structure? |
These are the trade-offs that make me less inclined to adopt this approach:
If the main headache is the selective use of the library at sub reducers, maybe an alternative to this is to make it more practical to wrap the combined reducers at the top level with redux-undo. I.e. we could make it easier to specify which subsections of the tree you want to track in the undo history--that way, any refactoring is minimized to top-level state access |
@nosovsh for the time being, would the following sort of filtering helper be useful at all? // say this is included in the library or in your code somewhere
export function basicGetIn(state, path) {
return (state) => {
let currentObj = state
for (const key of path) {
if (typeof currentObj === 'undefined') {
return undefined
} else {
currentObj = currentObj[key]
}
}
return currentObj
}
}
// accessFunc could be the return value of basicGetIn
export function includeSubsection(accessFunc) {
return (action, state, prevHist) => (
accessFunc(state) !== accessFunc(prevHist._latestUnfiltered)
)
} The use case would be the following: import undoable, { includeSubsection, basicGetIn } from 'redux-undo'
// assume we only want to undo the following portions of the undoable state:
// {
// 'userState1': {...I want to undo this state...}
// 'nested': {state: {with: {user: {state: {...I want to undo this state...}}}}},
// ...I want redux-undo to not update the past with remaining keys...
// }
// necessary because combineFilters is an 'and' operation
const orFilters = (...filters) => {
return filters.reduce(
(prev, curr) => (action, state, prevHist) => prev(action, state, prevHist) || curr(action, state, prevHist),
() => false
}
}
const finalReducer = undoable(reducer, {
filter: orFilters(
includeSubsection(basicGetIn(['userState1'])),
includeSubsection(basicGetIn(['nested', 'state', 'with', 'user', 'state']))
)
}) In theory you could also pass a memoized selector as the argument to EDIT: Made a few updates to the code and switched from combineFilters since inclusions are OR'd rather than AND'ed |
Do I understand correctly that you suggest to put this at top level app reducer? And inside filter put all paths I need to make undoable?
right? |
Yes. Or the highest level it needs to be, wherever you plan to put it. That way there are fewer |
Also updated the |
But that means that all paths in app in connect/selectors should be updated even which don't need undo :) I come up to the following reducer-wrapper that I can use in my code which will preserve my old state structure and will add undo state. What do you thing? // myReducer.js
// original reducer
const myReducer = (state={}, action) => ...
// reducer wrapper with undoable state
export default (state={}, action) => {
const newUndoableState = undoable(myReducer)(state.undoable, action);
if (newUndoableState === state.undoable) {
return state;
} else {
return {
...undoableState.present,
undoable: newUndoableState,
}
}
} |
Hmm // myReducer.js
// original reducer
const myReducer = (state={}, action) => ...
// the undoable portion
const undoableReducer = undoable(myReducer)
// reducer wrapper with undoable state
export default (state={}, action) => {
const newUndoableState = undoableReducer(state.undoable, action);
if (newUndoableState === state.undoable) {
return state;
} else {
return {
...newUndoableState.present,
undoable: newUndoableState,
}
}
} Let me know if that gives you the desired behavior--should merely copy the references of the values in |
Thanks for tips. I will try it in real project this week |
This can be solved simply: {
...currentState,
[PAST_KEY]: [...],
[FUTURE_KEY]: [...]
}
export const PAST_KEY = '@@reduxUndo/past';
export const FUTURE_KEY = '@@reduxUndo/future'; |
@dbkaplun that solves one of the problems. however, there are still other tradeoffs, as mentioned by @davidroeca earlier here: #179 (comment) if we can find a solution to these issues, it would be great if we could switch to a flat structure, as it would make it even easier to integrate redux-undo. |
For anyone stumbling across this, here's a solution I came up with that's similar to @nosovsh 's before I found this thread. export const undoer = (reducer) => ({ past = [], future = [], ...present } = {}, action) => {
const newReducer = undoable(reducer, { OPTIONS });
const newState = newReducer({ past, present, future }, action);
return {
...newState.present,
past: newState.past || [],
future: newState.future || []
};
}; You wrap your main/root reducer with this It takes the incoming state (first argument, before |
I took a different approach: import { StateWithHistory } from "redux-undo"
type WrappedSelectorFnT<S, R> = (state: StateWithHistory<S> | S) => R
type SelectorFnT<S, R> = (state: S) => R
export const wrapPresentStateSelector = <S, R>(
selector: SelectorFnT<S, R>
): WrappedSelectorFnT<S, R> => {
return (state: StateWithHistory<S> | S): R => {
return selector(presentState(state))
}
}
export const presentState = <T,>(state: StateWithHistory<T> | T): T => {
if (stateHasHistory(state)) {
return state.present
} else {
return state
}
}
const stateHasHistory = <T,>(state: StateWithHistory<T> | T): state is StateWithHistory<T> => {
return (
"past" in state
&& "present" in state && Array.isArray((state as StateWithHistory<T>).past)
&& "future" in state && Array.isArray((state as StateWithHistory<T>).future)
)
} You can write your selector as if the state isn't undoable, then wrap it with import { StateWithHistory } from "redux-undo"
import { presentState, wrapPresentStateSelector } from "./undoable"
interface State {
one_fish: number;
two_fish: number;
red_fish: string;
blue_fish: string;
}
type UndoableState = StateWithHistory<State>
const rawSelector = (state: State): string => {
return `${state.one_fish} ${state.red_fish}, ${state.two_fish} ${state.blue_fish}`
}
const wrappedSelector = wrapPresentStateSelector(rawSelector)
describe("undoable", () => {
describe("state", () => {
it("should return the raw state itself", () => {
const raw_state: State = {
one_fish: 1,
two_fish: 2,
red_fish: "red",
blue_fish: "blue",
}
const unwrapped_state = presentState(raw_state)
expect(unwrapped_state).toEqual(raw_state)
})
it("should return the raw state from the wrapped state", () => {
const raw_state: State = {
one_fish: 1,
two_fish: 2,
red_fish: "red",
blue_fish: "blue",
}
const undoable_state: UndoableState = {
past: [],
present: raw_state,
future: [],
}
const unwrapped_state = presentState(undoable_state)
expect(unwrapped_state).toEqual(raw_state)
})
})
describe("selector", () => {
it("should use the raw state itself", () => {
const raw_state: State = {
one_fish: 1,
two_fish: 2,
red_fish: "red",
blue_fish: "blue",
}
const result = wrappedSelector(raw_state)
expect(result).toEqual("1 red, 2 blue")
})
it("should use the raw state from the wrapped state", () => {
const raw_state: State = {
one_fish: 1,
two_fish: 2,
red_fish: "red",
blue_fish: "blue",
}
const undoable_state: UndoableState = {
past: [],
present: raw_state,
future: [],
}
const result = wrappedSelector(undoable_state)
expect(result).toEqual("1 red, 2 blue")
})
})
}) It's a little dicy if you give up Typescript checking and have |
Currently
redux-undo
adds one more level of hierarchy to your redux state:Maybe it's a good default but if you already have a big application and want to start using
redux-undo
in different places you will face a problem that you have to change paths to you state in all yourconnect
functions frompath.to.my.state
topath.to.my.state.present
. Which can cause a lot of bugs and anyway not a very pleasant thing to do. My idea was to add option toundoable
reducer that will force it to use such state structure instead:Setting option:
A far as I can see it can be easily implemented. What do you think?
The text was updated successfully, but these errors were encountered: