-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Update createReducer to accept a lazy state init function #1662
Changes from 2 commits
e6a1d48
fd31cdc
010bcf0
eaca9de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ temp/ | |
.tmp-projections | ||
build/ | ||
.rts2* | ||
coverage/ | ||
|
||
typesversions | ||
.cache | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,8 +98,10 @@ describe('createReducer', () => { | |
test('Freezes initial state', () => { | ||
const initialState = [{ text: 'Buy milk' }] | ||
const todosReducer = createReducer(initialState, {}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this now shows a valid concern with the lazyness. Imagine this situation:
so with a non-function being passed in, we still need to create a frozen copy of the item inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I agree there's some change in semantics here. On the other hand... the idea of the original state being mutated between the creation of the slice and the store being initialized seems almost nonexistent. Currently, what would happen is the initial state object gets frozen synchronously during the initial module import processing sequence, before any of the code in If we switch to this, you'd have:
So we're talking the same synchronous execution sequence, just moving the freezing to be somewhat later I suppose there's other possible edge cases, like using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could for example play a role in tests or situations where people accidentally create a new store on every render and just keep modifying |
||
const frozenInitialState = todosReducer(undefined, { type: 'dummy' }) | ||
|
||
const mutateStateOutsideReducer = () => (initialState[0].text = 'edited') | ||
const mutateStateOutsideReducer = () => | ||
(frozenInitialState[0].text = 'edited') | ||
expect(mutateStateOutsideReducer).toThrowError( | ||
/Cannot assign to read only property/ | ||
) | ||
|
@@ -132,6 +134,41 @@ describe('createReducer', () => { | |
behavesLikeReducer(todosReducer) | ||
}) | ||
|
||
describe('Accepts a lazy state init function to generate initial state', () => { | ||
const addTodo: AddTodoReducer = (state, action) => { | ||
const { newTodo } = action.payload | ||
state.push({ ...newTodo, completed: false }) | ||
} | ||
|
||
const toggleTodo: ToggleTodoReducer = (state, action) => { | ||
const { index } = action.payload | ||
const todo = state[index] | ||
todo.completed = !todo.completed | ||
} | ||
|
||
const lazyStateInit = () => [] as TodoState | ||
|
||
const todosReducer = createReducer(lazyStateInit, { | ||
ADD_TODO: addTodo, | ||
TOGGLE_TODO: toggleTodo, | ||
}) | ||
|
||
behavesLikeReducer(todosReducer) | ||
|
||
it('Should only call the init function when `undefined` state is passed in', () => { | ||
const spy = jest.fn().mockReturnValue(42) | ||
|
||
const dummyReducer = createReducer(spy, {}) | ||
expect(spy).not.toHaveBeenCalled() | ||
|
||
dummyReducer(123, { type: 'dummy' }) | ||
expect(spy).not.toHaveBeenCalled() | ||
|
||
const initialState = dummyReducer(undefined, { type: 'dummy' }) | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
}) | ||
}) | ||
|
||
describe('given draft state from immer', () => { | ||
const addTodo: AddTodoReducer = (state, action) => { | ||
const { newTodo } = action.payload | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be my suggestion to be a bit more bullet-proof there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically that doesn't prevent a user-provided
initialState()
function from returning a non-frozen value / mutable, I thinkThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, the call to
getInitialState
would not be frozen then. Yeah, that needs anothercreateNextState
call thenThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, we'd have to defer the
createNextState
call until usage time, in which case the external state could still get mutated in the meantime?Still feels like we're trying to solve a problem that's outside our scope here, tbh.
Here's what I've got atm:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone willingly provides a "lazy initializer" function, I guess they expect that it returns the value at the time of call, not something from before, so I'd say that now it looks like exactly the expected behaviour.