Repository: reduxjs/redux-toolkit

Problem statement:
Add createReducer() with init-state-as-a-lazy-getter overload
In my project, I have a piece of state that may be preloaded from the local storage:
```js
import { loadState } from './storage';

export default createReducer(loadState(), builder => ...);
```
The issue comes when I try to test this reducer because I need to isolate modules (I use Jest, but it may be relevant for other testing libraries, I believe). Without RTK it would be possible to write
```js
function reducer(state = loadState(), builder) { ... }
```
and the `loadState()` would be called every time the reducer is called with `undefined` as the first argument, but using RTK is much more convenient overall.

So is it possible to add `createState<S>(initState: () => S, ...)` overload so that a lazy getter could be passed?


Hints:
Hmm. A few thoughts:

- Seems like a mostly reasonable request in general
- I could see this requiring some complexity with the types and overloads
- That would also have to carry through to `createSlice`
- On the other hand: testing reducers is usually just `const actual = reducer(testState, action)`, in which case it doesn't matter what the initial state was.  What's the issue with testing in regards to the initial state here?
Since the variables declared in the module can't be changed after it was imported, I can't just mock `loadState()` with two different values and call the reducer with `undefined` test state after each mock. So I need to write some complex code like this:
```js
/* ./reducer.js */
import { loadState } from './storage';
export default createReducer(loadState() || '', builder => ...);

/* ./reducer.test.js */
import reducer from './reducer'
jest.mock('./storage', () => ({ loadState: jest.fn() });

// isolated imports with mocked value in storage
const preloadedState = 'some state';
let isolatedReducerWithLoadedState;
jest.isolateModules(() => {
  require('./storage').loadState.mockReturnValue(preloadedState);
  isolatedReducerWithTruthyInitState = require('./reducer').default;
});

// plain imports assuming there's no value stored
const reducer = require('./reducer').default;

describe('initState', () => {
  test('no preloaded state', () => {
    expect(reducer(undefined, {})).toBe('');
  });

  test('preloaded state', () => {
    expect(isolatedReducerWithLoadedState(undefined, {}).toBe(preloadedState);
  });
});
```
Partially it is a problem of jest, but they [resolved the issue](https://github.com/facebook/jest/issues/7863), and I missed it somehow, so the code will become a bit simpler. On the other hand, it will be even cleaner if I don't need to use `isolateModules()` at all.

Maybe this problem has a much better solution, but this is what I use now
Hmm. My question is, why is it even necessary to test that the reducer returns the initial state? That's basically testing _our_ library code implementation, rather than your app logic.
In fact, the initial state in my app is not that simple and depends on configuration, etc. So it's more about testing if the code combines data from the storage right.
Okay. Going back to the actual proposed change: how does passing a function actually help in this case?  It's going to be the same function all the time, even if it's being called later. Won't you have the same re-import / mocking issue regardless?
I see it like this:
```js
/* ./reducer.js */
import { loadState, loadAnotherState } from './storage';

function getInitState() {
  const state = loadState();
  const anotherState = loadAnotherState();
  const combined = /* some complex combination */
  return combined;
}

export default createReducer(getInitState, builder => ...);

/* ./reducer.test.js */
import { loadState, loadAnotherState } from './storage';
import { reducer } from './reducer';

describe('initState', () => {
  test('no preloaded state', () => {
    expect(reducer(undefined, {})).toBe('');
  });

  test('preloaded state', () => {
    loadState.mockReturnValue('value');
    loadAnotherState.mockReturnValue('another value');

    expect(isolatedReducerWithLoadedState(undefined, {}).toBe(properlyCombinedState);

    loadState.mockReset();
    loadAnotherState.mockReset();
  });
});
```
And that's it. no isolation, no module reinitialization. The initial state will be recomputed every time I call the reducer.

Since the combination logic is not exported, I can't test it directly. But I want to check if it works as expected.
You are jumping a lot of hooks to write a unit test for your combination logic. Why not just add an `export` keyword there? Maybe mark it as `/** @internal */` and that's that.
Because it doesn't feel right. *Export just for testing* smells a lot for me. I'm a kinda test noob and probably don't get how things should be done, so I follow intuition and some aesthetics considerations.
Testing a function by calling it implicitly through another function doesn't look much better to me :confused: 
> What will be the initial state if the local storage data happens to be *this*

Isn't it better?
So ironically I actually did run into this particular use case myself not too long ago - a slice where we're persisting the data from `localStorage` and loading it in the slice file, and it did make testing kind of a pain.

I think my biggest question would be much more complex this might make the types for `createSlice`.  Lenz, any thoughts?
Type-wise it would not be a problem, but we should be very clear on what it does. 

Does it execute once when `undefined` is passed in? Every time?

Tbh I was also pondering to expose `slice.initalState` so people can write it inline and not have to export it, for use like `useReducer(slice.reducer, slice.initialState)` - but those would probably kinda conflict then.
Suggestion from Lenz in chat:

> * allow `initialState` to also be `() => State`
> * add `slice.getInitialState()` which either just returns the initial state or executes the lazy initializer
That should be very little type change and that second change would be a good quality of life improvement for `useReducer` users.
> 
> `useReducer(slice.reducer, undefined, slice.getInitialState)`
>
> Nice side effect: we can tunnel `initialState` through `produce` once, so it returns a frozen object. If people define `initialState` as a constant, there is always a risk they manipulate that accidentally

Patch:
diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts
--- a/packages/toolkit/src/createReducer.ts
+++ b/packages/toolkit/src/createReducer.ts
@@ -66,6 +66,16 @@ export type CaseReducers<S, AS extends Actions> = {
   [T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
 }
 
+export type NotFunction<T> = T extends Function ? never : T
+
+function isStateFunction<S>(x: unknown): x is () => S {
+  return typeof x === 'function'
+}
+
+export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
+  getInitialState: () => S
+}
+
 /**
  * A utility function that allows defining a reducer as a mapping from action
  * type to *case reducer* functions that handle these action types. The
@@ -84,8 +94,8 @@ export type CaseReducers<S, AS extends Actions> = {
  * That builder provides `addCase`, `addMatcher` and `addDefaultCase` functions that may be
  * called to define what actions this reducer will handle.
  *
- * @param initialState - The initial state that should be used when the reducer is called the first time.
- * @param builderCallback - A callback that receives a *builder* object to define
+ * @param initialState - `State | (() => State)`: The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
+ * @param builderCallback - `(builder: Builder) => void` A callback that receives a *builder* object to define
  *   case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
  * @example
 ```ts
@@ -105,7 +115,7 @@ function isActionWithNumberPayload(
   return typeof action.payload === "number";
 }
 
-createReducer(
+const reducer = createReducer(
   {
     counter: 0,
     sumOfNumberPayloads: 0,
@@ -130,10 +140,10 @@ createReducer(
 ```
  * @public
  */
-export function createReducer<S>(
-  initialState: S,
+export function createReducer<S extends NotFunction<any>>(
+  initialState: S | (() => S),
   builderCallback: (builder: ActionReducerMapBuilder<S>) => void
-): Reducer<S>
+): ReducerWithInitialState<S>
 
 /**
  * A utility function that allows defining a reducer as a mapping from action
@@ -151,7 +161,7 @@ export function createReducer<S>(
  * This overload accepts an object where the keys are string action types, and the values
  * are case reducer functions to handle those action types.
  *
- * @param initialState - The initial state that should be used when the reducer is called the first time.
+ * @param initialState - `State | (() => State)`: The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
  * @param actionsMap - An object mapping from action types to _case reducers_, each of which handles one specific action type.
  * @param actionMatchers - An array of matcher definitions in the form `{matcher, reducer}`.
  *   All matching reducers will be executed in order, independently if a case reducer matched or not.
@@ -164,6 +174,14 @@ const counterReducer = createReducer(0, {
   increment: (state, action) => state + action.payload,
   decrement: (state, action) => state - action.payload
 })
+
+// Alternately, use a "lazy initializer" to provide the initial state
+// (works with either form of createReducer)
+const initialState = () => 0
+const counterReducer = createReducer(initialState, {
+  increment: (state, action) => state + action.payload,
+  decrement: (state, action) => state - action.payload
+})
 ```
  
  * Action creators that were generated using [`createAction`](./createAction) may be used directly as the keys here, using computed property syntax:
@@ -180,31 +198,38 @@ const counterReducer = createReducer(0, {
  * @public
  */
 export function createReducer<
-  S,
+  S extends NotFunction<any>,
   CR extends CaseReducers<S, any> = CaseReducers<S, any>
 >(
-  initialState: S,
+  initialState: S | (() => S),
   actionsMap: CR,
   actionMatchers?: ActionMatcherDescriptionCollection<S>,
   defaultCaseReducer?: CaseReducer<S>
-): Reducer<S>
+): ReducerWithInitialState<S>
 
-export function createReducer<S>(
-  initialState: S,
+export function createReducer<S extends NotFunction<any>>(
+  initialState: S | (() => S),
   mapOrBuilderCallback:
     | CaseReducers<S, any>
     | ((builder: ActionReducerMapBuilder<S>) => void),
   actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
   defaultCaseReducer?: CaseReducer<S>
-): Reducer<S> {
+): ReducerWithInitialState<S> {
   let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
     typeof mapOrBuilderCallback === 'function'
       ? executeReducerBuilderCallback(mapOrBuilderCallback)
       : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
 
-  const frozenInitialState = createNextState(initialState, () => {})
+  // Ensure the initial state gets frozen either way
+  let getInitialState: () => S
+  if (isStateFunction(initialState)) {
+    getInitialState = () => createNextState(initialState(), () => {})
+  } else {
+    const frozenInitialState = createNextState(initialState, () => {})
+    getInitialState = () => frozenInitialState
+  }
 
-  return function (state = frozenInitialState, action): S {
+  function reducer(state = getInitialState(), action: any): S {
     let caseReducers = [
       actionsMap[action.type],
       ...finalActionMatchers
@@ -257,4 +282,8 @@ export function createReducer<S>(
       return previousState
     }, state)
   }
+
+  reducer.getInitialState = getInitialState
+
+  return reducer as ReducerWithInitialState<S>
 }
diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts
--- a/packages/toolkit/src/createSlice.ts
+++ b/packages/toolkit/src/createSlice.ts
@@ -8,7 +8,7 @@ import type {
 } from './createAction'
 import { createAction } from './createAction'
 import type { CaseReducer, CaseReducers } from './createReducer'
-import { createReducer } from './createReducer'
+import { createReducer, NotFunction } from './createReducer'
 import type { ActionReducerMapBuilder } from './mapBuilders'
 import { executeReducerBuilderCallback } from './mapBuilders'
 import type { NoInfer } from './tsHelpers'
@@ -53,6 +53,12 @@ export interface Slice<
    * This enables reuse and testing if they were defined inline when calling `createSlice`.
    */
   caseReducers: SliceDefinedCaseReducers<CaseReducers>
+
+  /**
+   * Provides access to the initial state value given to the slice.
+   * If a lazy state initializer was provided, it will be called and a fresh value returned.
+   */
+  getInitialState: () => State
 }
 
 /**
@@ -71,9 +77,9 @@ export interface CreateSliceOptions<
   name: Name
 
   /**
-   * The initial state to be returned by the slice reducer.
+   * The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
    */
-  initialState: State
+  initialState: State | (() => State)
 
   /**
    * A mapping from action types to action-type-specific *case reducer*
@@ -301,5 +307,6 @@ export function createSlice<
     reducer,
     actions: actionCreators as any,
     caseReducers: sliceCaseReducersByName as any,
+    getInitialState: reducer.getInitialState,
   }
 }


Test patch:
diff --git a/packages/toolkit/src/tests/createReducer.test.ts b/packages/toolkit/src/tests/createReducer.test.ts
--- a/packages/toolkit/src/tests/createReducer.test.ts
+++ b/packages/toolkit/src/tests/createReducer.test.ts
@@ -98,8 +98,10 @@ describe('createReducer', () => {
     test('Freezes initial state', () => {
       const initialState = [{ text: 'Buy milk' }]
       const todosReducer = createReducer(initialState, {})
+      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
diff --git a/packages/toolkit/src/tests/createSlice.test.ts b/packages/toolkit/src/tests/createSlice.test.ts
--- a/packages/toolkit/src/tests/createSlice.test.ts
+++ b/packages/toolkit/src/tests/createSlice.test.ts
@@ -63,6 +63,48 @@ describe('createSlice', () => {
       expect(caseReducers.increment).toBeTruthy()
       expect(typeof caseReducers.increment).toBe('function')
     })
+
+    it('getInitialState should return the state', () => {
+      const initialState = 42
+      const slice = createSlice({
+        name: 'counter',
+        initialState,
+        reducers: {},
+      })
+
+      expect(slice.getInitialState()).toBe(initialState)
+    })
+  })
+
+  describe('when initialState is a function', () => {
+    const initialState = () => ({ user: '' })
+
+    const { actions, reducer } = createSlice({
+      reducers: {
+        setUserName: (state, action) => {
+          state.user = action.payload
+        },
+      },
+      initialState,
+      name: 'user',
+    })
+
+    it('should set the username', () => {
+      expect(reducer(undefined, actions.setUserName('eric'))).toEqual({
+        user: 'eric',
+      })
+    })
+
+    it('getInitialState should return the state', () => {
+      const initialState = () => 42
+      const slice = createSlice({
+        name: 'counter',
+        initialState,
+        reducers: {},
+      })
+
+      expect(slice.getInitialState()).toBe(42)
+    })
   })
 
   describe('when mutating state object', () => {