I was learning Redux and came across createStore function. So, as I understood createStore receives 3 parameters:
reducer
initial state
enhancers (for simplicity we will use only middlewares)
But when we use createStore in action we do not pass initial state as the second argument BUT pass reducer with default state like this:
const initialState = {counter:0}
const reducer =(state=initialState, action)=>...
The question is why don't we put the initial state as the second argument but pass initialState to reducer?
I think you are confusing the initial state of a reducer to that of the global state of your app.
Global state simply means that combined state of all the reducers in your app.
For simplicity let's just assume you only have one reducer in your app.
Reducer :
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text])
default:
return state
}
}
So this simple function todos is our reducer which will give us the current state tree, whenever it is run.
So this is our first parameter to createStore.
Initial State :
['Understanding Store']
Let's assume our initial state as an array which contains 1 item as shown above.
This will be our second parameter to createStore.
Now we create our store like this:
import { createStore } from 'redux'
//... code
.
.
.
const store = createStore(todos, ['Understanding Store'])
Now our store is created.
Nothing fancy, store is basically an object, which has few methods on it.
One of those methods is dispatch.
This method helps in dispatching an action, which will run through our reducer and then update the state.
So when we do this
store.dispatch({
type: 'ADD_TODO',
text: 'Learn methods on Store'
})
This will update our state as below:
['Understanding Store','Learn methods on Store']
But when your app grows big, you might want to create different functions (reducers) to manage different parts of your global state.
If we have one more reducer, say counter.js :
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
Then to combine our 1st reducer todos and this counter reducer, we have an utility called combineReducer.
rootReducer.js
import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'
export default combineReducers({
todos,
counter
})
Then using createStore, you just do this:
import { createStore } from 'redux'
import rootReducer from './rootReducer.js;
const store = createStore(rootReducer);
There are certain rules that you need to follow while using combineReducers.
Read the rules here
The use case for passing an initial state as the second argument in createStore is intended for use cases where you get this initial state from the outside when loading your app. Examples could be state generated on the server for server-side rendered applications that are hydrated on the client, or an application that restores the redux state form local-storage when loaded.
The initial value of a single reducer should be returned when the reducer function is called with undefined state, the easiest way is to use a default argument for state:
const reducer = (state = initialState, action) => ...
This allows you to define the initialState close to where the reducer is defined and it scales nicely with combineReducers when you have a larger number of reducers. If you would put all the initialState of all reducers into one object that is passed to createStore this would become difficult to keep in sync.
I think you have actually confused createStore with a reducer. Think of createStore as a function which returns you a collection of reducers by adding in a certain middleWares and other functionalities like dispatch
More often than not you have multiple reducers in your app and you actually combine them using combineReducers
Say for example you combineReducers is
import userReducer from 'reducers/user';
import authReducer from 'reducers/auth';
import countReducer from 'reducers/count';
const reducers = combineReducers({
userReducer,
authReducer,
countReducer,
});
Now the initialState to createStore must be of the format of an object with keys as userReducer, authReducer, countReducer and then the individual reducers state. For example
{
userReducer: { id: 1, name: 'Test'},
authReducer: { isLoading: true, isAuthenticated: false},
countReducer: {counter: 0}
}
Now thing of the second keys as equivalent to initialState each individual reducer
For example: reducer/count.js
const initialState = {counter:0}
const reducer =(state=initialState, action)=>...
The way it works is that createStore would actually call the reducer with the action each time action is invoked like
reducer(state, action);
In case of combineReducer it works like below
const combineReducers = (reducers) => {
return (state, action) => {
const tempState = { ...state };
Object.keys(reducers).forEach((key) => {
tempState[key] = reducers[key](tempState[key], action);
});
return tempState;
};
};
and for the initial time it invokes it with
reducer(initialState, {type: "##redux/INIT"});
so that each reducer's initial state is populated
P.S. If you do not pass the initialState to createStore, each reducer takes in the default argument passed to to it like const reducer =(state=initialState, action)=> and returns state for default switch clause causing the initialState from each reducer to be used
Related
Hey fellow programmers,
Been having fun learning react-redux lately, but I do have one question that bothers me.
My understanding is that, by using createAsyncThunk it will automatically generates action type constants. (pending, fulfilled, and rejected)
What I wanted to know is that is there any way to manually dispatch action type during createAsyncthunk , so that we can have more flexibility in our code.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId).then(
...
dispatch(fulfilled) // is this possible ?
).catch(
dispatch(rejected) // is this possible ?
)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
...,
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[fetchUserById.fulfilled]: (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
}
}
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
The point of createAsyncThunk is that it generates those action types, _and dispatches them for you automatically. You definitely do not need to dispatch(fulfilled()) yourself, because that's what createAsyncThunk does for you - you just need to return a promise that either resolves or reject, and it dispatches the fulfilled/rejected action types based on that.
You do get access to thunkAPI.dispatch, so you can dispatch other actions if necessary, but you don't need to worry about the fulfilled/rejected actions yourself.
I have a react/redux app with store set up like this:
interface AppState {
substateA: SubstateA;
substateB: SubstateB;
}
And two reducers managing their individual states:
const reducer = combineReducers<AppState>({
substateA: reducerA,
substateb: reducerB,
});
What I would like to do as add a reducer that manages whole state additionally to those 2 reducers. How can I do that or is there other better solution?
A reducer is simply a function that takes state and an action and returns a new state object.
combineReducers returns a reducer which calls each of your reducers with only their slice of the state.
Have a play with something like this.. you may want it to run the combined reducers first, or your global state reducer first, depending on what you are doing.
const combinedReducer = combineReducers<AppState>({
substateA: reducerA,
substateb: reducerB,
});
const reducer = (state, action) => {
state = reducerC(state, action);
return combinedReducer(state, action);
}
I have a store which is shaped like this:
{
// ...data
user: {
warranties: {
W_1: ['O_1', 'O_2'],
W_2: ['O_3', 'O_4']
}
}
}
Where keys starting with W_ are warranties, keys starting with O_ are options.
For each warranty I have one or more options associated to it, relations in user.warranties are in the form: warranty => [options].
To achieve it I'm combining my reducers like this:
rootReducer = combineReducers({
// ...other main reducers
user: combineReducers({
// ...other user reducers
warranties
})
})
Now, the "problem" is that both USER_WARRANTY and USER_OPTION actions are handled by the same reducer, because:
When I add an option, I need to push it to the correct warranty entry.
Inversely when I add a warranty I need to populate it with its default options.
And ultimately, they operate on the same slice of data
So the warranties reducer, has to react to both actions, looking like this:
export default function warranties(state = {}, action) {
switch (action.type) {
case USER_WARRANTIES_ADD:
// add warranty key to `user.warranties`
case USER_WARRANTIES_REMOVE:
// remove warranty key from `user.warranties`
case USER_OPTIONS_ADD:
// push option to `user.warranties[warrantyID]`
case USER_OPTIONS_REMOVE:
// remove option from `user.warranties[warrantyID]`
default:
return state
}
}
I would like to split this in two reducers, warranties and options, but still have them operate on the same slice of data.
Ideally I would then compose my root reducer like this:
rootReducer = combineReducers({
// ...other main reducers
user: combineReducers({
// ...other user reducers
warranties: magicalCombine({
warranties,
options
})
})
})
Where magicalCombine is the function I am having troubles to find.
I have tried reduce-reducers, but looks like the second reducer (options) is never actually reached, and I'm actually unsure about it since I'm not trying to achieve flat state, but actually operate on the same key.
A reducer is simple a function that take state and action and returns a new state object, so I think this would do what you want..
rootReducer = combineReducers({
// ...other main reducers
user: combineReducers({
// ...other user reducers
warranties: (state, action) => {
// state is state.user.warranties
// we pass it to each reducer in turn and return the result
state = warranties(state, action);
return options(state, action);
}
})
})
using reduceReducers should do the same thing (I haven't used it before, but that's what it looks like..)
rootReducer = combineReducers({
// ...other main reducers
user: combineReducers({
// ...other user reducers
warranties: reduceReducers(warranties, options)
})
})
combineReducers from redux is simply intentionally restricted to pass only the value of the state property that matches the key in the reducers object provided to it, it's not really special in any other way. see more here.. https://redux.js.org/recipes/structuringreducers/beyondcombinereducers
I am new to redux architecture, I have this basic doubt can we update the reducers list after creating the store using combinedReducer and createStore methods?
Yes, you can update the reducers and inject a new one asynchronously with replaceReducer api of Redux store.
It is an advanced API. You might need this if your app implements code
splitting, and you want to load some of the reducers dynamically. You
might also need this if you implement a hot reloading mechanism for
Redux.
Take as example this starter-kit
In createStore.js file the reducers passed as arguments to the createStore method are the result of makeRootReducers(). Pay attention to the fact that no one async reducer have been passed to this function.
// extract of src/store/createStore.js
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk'
import makeRootReducer from './reducers'
export default (initialState = {}, history) => {
// ...
// ======================================================
// Store Instantiation and HMR Setup
// ======================================================
const store = createStore(
makeRootReducer(), // <------------- without arguments, it returns only the synchronously reducers
initialState,
compose(
applyMiddleware(...middleware),
...enhancers
)
)
store.asyncReducers = {}
// ...
}
In reducers.js file:
makeRootReducer function calls combineReducers with the default reducers
needed for the startup (like router reducer) and other "asynchronously" reducers passed as arguments
injectReducer is a function called for injecting new reducers on runtime. It call replaceReducer api on the store passing as argument a new list of reducers obtain through makeRootReducer(async) function
see below:
// src/store/reducers.js
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
export const makeRootReducer = (asyncReducers) => {
return combineReducers({
// Add sync reducers here
router,
...asyncReducers
})
}
export const injectReducer = (store, { key, reducer }) => {
store.asyncReducers[key] = reducer
store.replaceReducer(makeRootReducer(store.asyncReducers))
}
export default makeRootReducer
Finally, in the starter-kit the reducer is injected on route definition, like here:
// src/routes/Counter/index.js
import { injectReducer } from '../../store/reducers'
export default (store) => ({
path: 'counter',
/* Async getComponent is only invoked when route matches */
getComponent (nextState, cb) {
/* Webpack - use 'require.ensure' to create a split point
and embed an async module loader (jsonp) when bundling */
require.ensure([], (require) => {
/* Webpack - use require callback to define
dependencies for bundling */
const Counter = require('./containers/CounterContainer').default
const reducer = require('./modules/counter').default
/* ----> HERE <---- */
/* Add the reducer to the store on key 'counter' */
injectReducer(store, { key: 'counter', reducer }) // <-------
/* Return getComponent */
cb(null, Counter)
/* Webpack named bundle */
}, 'counter')
}
This technique is helpful when you want split a large app and avoid to load all the reducers at the boot.
Getting warning: "Using Maps as children is not yet fully supported" when combining reducers (each with its own immutable.js Map) with #redux combineReducers. What's a better store structure to avoid this?
import reducer_2 from './action_handlers/two';
import reducer_3 from './action_handlers/three';
import { combineReducers } from 'redux';
let reducer = combineReducers({
reducer_2,
reducer_3
});
export default reducer;
//=====================
// ./action_handlers/two.js
const INITIAL_STATE_2 = new Map({
foo: 'bar'
});
export default function reducer_2(state = INITIAL_STATE_2, action) {...}
//======================
// ./action_handlers/three.js
const INITIAL_STATE_3 = new Map({
baz: 'bat'
});
export default function reducer_3(state = INITIAL_STATE_3, action) {...}
//=======================
Looks like you’re using ES6’s Map instead of Immutable’s.
Write the following line of code in both your reducers:
import { Map } from 'immutable';
and then
const INITIAL_STATE_2 = Map({
foo: 'bar'
});
Or you can use
import Immutable from 'immutable';
const INITIAL_STATE_2 = Immutable.Map({
foo: 'bar'
});
It is not really clear whether you are relating your question to the Immutable.js Map data structure or to the regular Map object.
However, in both cases Redux's combineReducers creates a plain object as root-state. If you are using Immutable.js, there exists the redux-immutable package which provides a combineReducers function that returns an Immutable.js data structure instead of an object.
You can't user combine reducer of redux
a combineReducers function that comes with Redux will not work as it doesn’t know how to read and create new versions of state.