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);
}
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 am coming across a reusability conundrum in my React/Redux app. I have coded reusable React components, such as dropdowns and sliders, and I am storing the state values in my Redux Store. It is dawning on me now that anytime any JSX instance of the component changes state, all other instances of that component also change state. They all begin to mimic each other, whereas I need them to operate independently. I also want to initialize state differently in the different instances of the component, and can't figure out a way to do this. For example, for a slider, I want the beginning and ending value of the slider range to be different in different instances of the component. This would be easy to do through React's normal props system, but I am getting confused on how to do it via Redux.
I've researched this a bit and it seems the solution is to add unique IDs to each instance of the component in the Redux store. But I need specifics on where to add the ID (the reducer? the action creator? the component?) and how to have everything flow so that the right component is getting their state changed.
This is my reducer:
const INITIAL_STATE = {
slots: 100,
step: 5,
start: 35,
end: 55,
};
const sliderReducer = (state=INITIAL_STATE, action ) => {
switch (action.type) {
case "MIN_SELECTED":
console.log("Min Selected");
return {
...state,
start: action.payload
};
case "MAX_SELECTED":
console.log("Max Selected");
return {
...state,
end: action.payload
};
default:
return state;
}
}
export default sliderReducer;
this is my action creator:
//Slider Minimum Value Selection Action Creator
export const selectMinValue = (value) => {
return (dispatch) => {
dispatch({
type: "MIN_SELECTED",
payload: value
});
};
};
//Slider Maximum Value Selection Action Creator
export const selectMaxValue = (value) => {
return (dispatch) => {
dispatch({
type: "MAX_SELECTED",
payload: value
});
};
};
the component code is too long to paste, but it's event handlers for onDrag events and JSX for the slider essentially.
For anyone here who runs into the same problem, there are many libraries which attempt to solve this, in my opinion, in too complex a way. The cleanest and simplest way to resolve the issue is to associate a namespace identifier with each instance of the component in the redux store. The best answer I found is here: having multiple instance of same reusable redux react components on the same page/route
Good luck!
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
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
How is it possible to save a function as state in redux store?
Example:
I pass a function as parameter to a redux-thunk dispatcher function and i want to save this filter function in my redux store:
export const SET_FILTERED_USERS = 'SET_FILTERED_USERS';
export function setFilteredUsers(filter) {
return (dispatch, getState) => {
const allUsers = getState().users.allUsers;
const filteredUsers = allUsers.filter(filter);
dispatch({
type: SET_FILTERED_USERS,
data: {
filteredUsers,
filter
}
});
const activeUser = getState().users.activeUser;
if (activeUser && !_.isEmpty(filteredUsers) && filteredUsers.indexOf(activeUser._id) === -1) {
dispatch(setActiveUser(filteredUsers[0]));
} else {
dispatch(setActiveUser(allUsers[0]));
}
}
}
In ReduxDevTools i can see, "filter" is not dispatched and saved in store. Is there a way to do this?
Thanks
Update: my shortend reducer:
import {
SET_FILTERED_USERS
} from '../actions/users';
import assign from 'object-assign';
export const initialState = {
filter: null,
filteredUsers: null
};
export default function (state = initialState, action = {}) {
const {data, type} = action;
switch (type) {
case SET_FILTERED_USERS:
return assign({}, state, {
filteredUsers: data.filteredUsers,
filter: data.filter
});
default:
return state;
}
}
As Sebastian Daniel said, don't do that. Per the Redux FAQ, that breaks things like time-travel debugging, and is not how Redux is intended to be used: Can I put functions, promises, or other non-serializable items in my store state?
What you could consider as an alternative is storing some kind of description of the filtering you want. As a sort of relevant example, in my current prototype, I'm creating a lookup table of dialog classes that I might want to show, and when I need to show one, I dispatch an action containing the name of the dialog type. My dialog manager class pulls that "dialogType" field out of state, uses that to look up the correct dialog component class, and renders it.
The other question, really, is why you actually want to store a function in your state in the first place. I see what you're trying to do there, but not actually what you're hoping to accomplish with it.