I am using redux with multiple reducers combined into rootReducer. How is it possible to modify the state of one reducer from another reducer? Ex:
// systemReducer.js
const INITIAL_STATE = { isLoggedIn: true }
function systemReducer(state = INITIAL_STATE, action) {
switch(action.type) { ... }
}
// messagesReducer.js
const INITIAL_STATE = { messages: [] }
function messagesReducer(state = INITIAL_STATE, action) {
switch(action.type) { ... }
}
And then say I have action makers for messagesReducer as under:
// messageActions.js
export const messagesFetchAction = (data) => {
return {
type: MESSAGES_FETCH,
data: data
}
}
Now, how can I modify systemReducer's isLoggedIn to false from messagesFetchAction()? So it looks like as under for example:
// messageActions.js
export const messagesFetchAction = (data) => {
systemState.setState({isLoggedIn: false}); // <=====
return {
type: MESSAGES_FETCH,
data: data
}
}
If you have two reducers does not mean that you have several stores. You still have single store, but combined from two reducers. In general, your store may look like:
{
systemReducer: {
isLoggedIn: true
},
messagesReducer: {
messages: []
}
}
You couldn't dispatch action from reducer. This is prohibited by redux. But you may dispatch several actions from action creator. For example:
export const messagesFetchAction = (data) => (dispatch) => {
dispatch({type: LOGGED_IN, isLoggedIn: false});
dispatch({
type: MESSAGES_FETCH,
data: data
});
}
The action creator above is for Redux Thunk. To be able to use it, apply middleware when creating store like this
const rootReducer = combineReducers({
systemReducer,
messagesReducer
});
const store = createStore(rootReducer, applyMiddleware(
thunkMiddleware
));
Related
In redux actions, when we want to set a value, we use a type to dispatch like this :
dispatch({
type: SET_LOADER,
payload: true
})
Where the type: SET_LOADER stored in a different file and export it like below.
export const SET_LOADER = 'SET_LOADER'
And in reducer we will do it like this :
function initialState() {
return {
formErr: {},
isLoading: false
}
}
export default function (state = initialState(), action) {
const { type, payload } = action;
switch (type) {
case SET_LOADER:
return {
...state,
isLoading: payload
}
default:
return state
}
}
So in my application, I have this SET_LOADER type used in different actions and reducers. For example, in authentication, in profile update, when I want to load, I will use this type. So I have this type imported in various places.
I'm not sure if it's okay to use a single type for multipurpose because I noticed now that when I do dispatch, the redux state that get updated is not belonged to the target reducer. The state update is happening at different reducer.
But it's working for the first time dispatch. The next update, it's updating the incorrect redux state. After I refresh the page and try to update again, then it work.
first of all you need to separate your reducer into multiple reducers and then combine them in the store , then you can probably get away by using that same action in multiple cases for but then it'll be only a per reeducer solution meaning that let's say you have and Auth reducer this reducer will have its isLoading , and it may interfere with other actions within that reducer , fore example FetchAllProducts will use isLoading but also FetchByIdProduct is using isLoading and same for other actions that will trigger a loading state .
let's consider these reducers which use the same initial state
function initialState() {
return {
formErr: {},
isLoading: false
}
}
export const authReducer=(state = initialState(), action)=> {
const { type, payload } = action;
switch (type) {
case SET_LOADER:
return {
...state,
isLoading: payload
}
default:
return state
}
}
export const productsReducer=(state = initialState(), action)=> {
const { type, payload } = action;
switch (type) {
case SET_LOADER:
return {
...state,
isLoading: payload
}
default:
return state
}
}
export const cartReducer =(state = initialState(), action)=> {
const { type, payload } = action;
switch (type) {
case SET_LOADER:
return {
...state,
isLoading: payload
}
default:
return state
}
}
//this is the store
import {createStore,applyMiddleware,compose,combineReducers} from 'redux'
import thunk from 'redux-thunk'
import {productsReducer} from './reducers/ProductReducer'
import {cartReducer} from './reducers/CartReducer'
import {authReducer } from './reducers/AuthReducer'
const initialState={
products: {
formErr: {},
isLoading: false
},
cart: {
formErr: {},
isLoading: false
},
auth: {
formErr: {},
isLoading: false
}
}
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(combineReducers({
products: productsReducer,
cart : cartReducer ,
auth : authReducer,
}),
initialState,
composeEnhancer(applyMiddleware(thunk))
)
export default store
even though their using the same initial state you , when you will connect a component to the redux store you have access to three different isLoading :
export default connect((state)=>({
isLoading : state.products.isLoading,
isLoading2: state.authReducer.isLoading,
isLoading3: state.cart.isLoading,
}))(Products)
but to be honest I'd rather have make my actions more explicit and case specific something like productsFetchIsLoading , this gives you more control and prevents bugs
I noticed now that when I do dispatch, the redux state that get updated is not belonged to the target reducer. The state update is happening at different reducer.
Every action gets dispatched to every reducer. When you call dispatch({ type: SET_LOADER, payload: true }), the expected behavior is that the isLoading state will get set to true in every reducer which has a case SET_LOADER.
If you want the loading states to be independent then each reducer needs a unique string action type.
If you have multiple similar reducers then you can use a factory function to generate the type names, action creator functions, and reducer cases. Here we are extending the createSlice utility from Redux Toolkit.
We pass in the name which is the prefix for the auto-generated action types, the initialState of just the unique properties for this reducer state, and any unique reducer cases. This will get merged with the standard base state.
Helper:
const createCustomSlice = ({name, initialState = {}, reducers = {}}) => {
return createSlice({
name,
initialState: {
formErr: {},
isLoading: false
...initialState,
},
reducers: {
setLoader: (state, action) => {
state.isLoading = action.payload;
},
setFormErr: (state, action) => {
state.formErr = action.payload;
}
...reducers,
}
});
}
Usage:
const profileSlice = createCustomSlice({
name: "profile",
initialState: {
username: ""
},
reducers: {
setUsername: (state, action) => {
state.username = action.payload;
}
}
});
// reducer function
const profileReducer = profileSlice.reducer;
// action creator functions
export const { setFormErr, setLoader, setUsername } = profileSlice.actions;
These action creators will create actions with a prefixed type like 'profile/setLoader'.
I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())
I have seen solutions for clearing/resetting the store after logout but did not understand how to implement the same functionality for the following way of setting up the redux store.
Store.js:
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import authReducer from './ducks/authentication'
import snackbar from './ducks/snackbar'
import sidebar from './ducks/sidebar'
import global from './ducks/global'
import quickView from './ducks/quickView'
import profileView from './ducks/profileView'
const store = configureStore({
reducer: {
auth: authReducer,
snackbar,
sidebar,
global,
quickView,
profileView,
},
middleware: [...getDefaultMiddleware()],
})
export default store
Here is how all the reducers implemented using createAction and createReducer from #reduxjs/toolkit.
snackbar.js:
import { createAction, createReducer } from '#reduxjs/toolkit'
export const handleSnackbar = createAction('snackbar/handleSnackbar')
export const openSnackBar = (
verticalPosition,
horizontalPosition,
message,
messageType,
autoHideDuration = 10000
) => {
return async dispatch => {
dispatch(
handleSnackbar({
verticalPosition,
horizontalPosition,
message,
autoHideDuration,
messageType,
isOpen: true,
})
)
}
}
export const closeSnackbar = () => {
return dispatch => {
dispatch(handleSnackbar({ isOpen: false }))
}
}
const initialState = {
verticalPosition: 'bottom',
horizontalPosition: 'center',
message: '',
autoHideDuration: 6000,
isOpen: false,
messageType: 'success',
}
export default createReducer(initialState, {
[handleSnackbar]: (state, action) => {
const {
isOpen,
verticalPosition,
horizontalPosition,
message,
autoHideDuration,
messageType,
} = action.payload
state.isOpen = isOpen
state.verticalPosition = verticalPosition
state.horizontalPosition = horizontalPosition
state.message = message
state.autoHideDuration = autoHideDuration
state.messageType = messageType
},
})
As per Dan Abramov's answer, create a root reducer which will simply delegate the action to your main or combined reducer. And whenever this root reducer receives a reset type of action, it resets the state.
Example:
const combinedReducer = combineReducers({
first: firstReducer,
second: secondReducer,
// ... all your app's reducers
})
const rootReducer = (state, action) => {
if (action.type === 'RESET') {
state = undefined
}
return combinedReducer(state, action)
}
So, if you have configured your store with #reduxjs/toolkit's configureStore, it might look like this:
import { configureStore } from '#reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer,
// ... more reducers
},
});
where configureStore's first parameter reducer accepts a function (which is treated as root reducer) or an object of slice reducers which is internally converted to root reducer using combineReducers.
So, now instead of passing object of slice reducers (shown above), we can create and pass root reducer by ourselves, here is how we can do it:
const combinedReducer = combineReducers({
counter: counterReducer,
// ... more reducers
});
Now, lets create a root reducer which does our reset job when needed:
const rootReducer = (state, action) => {
if (action.type === 'counter/logout') { // check for action type
state = undefined;
}
return combinedReducer(state, action);
};
export default configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware()]
});
Here is CodeSandbox
I wanted to extend Ajeet's answer so that it is accessible to those who want complete type safety throughout their Redux store.
The key differences are that you need to declare a RootState type, which is documented in the RTK docs
const combinedReducer = combineReducers({
counter: counterReducer
});
export type RootState = ReturnType<typeof combinedReducer>;
And then in your rootReducer, where you are executing your logout function, you want to maintain type safety all the way down by giving the state param the RootState type, and action param AnyAction.
The final piece of the puzzle is setting your state to an empty object of type RootState instead of undefined.
const rootReducer: Reducer = (state: RootState, action: AnyAction) => {
if (action.type === "counter/logout") {
state = {} as RootState;
}
return combinedReducer(state, action);
};
I forked Ajeet's answer on CodeSandbox, added the required types, and you can view it here.
If you're looking to reset each slice to its initial state (unlike setting the entire state to an empty object) you can use extraReducers to respond to a logout action and return the initial state.
In auth.tsx:
const logout = createAction('auth/logout')
In foo.tsx:
const initialState = {
bar: false,
}
const fooSlice = createSlice({
name: 'foo',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(logout, () => {
return initialState
})
},
})
A simplified example with two reducers:
// actions and reducer for state.first
const resetFirst = () => ({ type: 'FIRST/RESET' });
const firstReducer = (state = initialState, action) => {
switch (action.type) {
// other action types here
case 'FIRST/RESET':
return initialState;
default:
return state;
}
};
// actions and reducer for state.second
const resetSecond = () => ({ type: 'SECOND/RESET' });
const secondReducer = (state = initialState, action) => {
switch (action.type) {
// other action types here
case 'SECOND/RESET':
return initialState;
default:
return state;
}
};
const rootReducer = combineReducers({
first: firstReducer,
second: secondReducer
});
// thunk action to do global logout
const logout = () => (dispatch) => {
// do other logout stuff here, for example logging out user with backend, etc..
dispatch(resetFirst());
dispatch(resetSecond());
// Let every one of your reducers reset here.
};
The simple solution - just add a reducer like this...
resetList: (state) => {
return (state = []);
},
... and call it with a button:
const handleResetList = () => {
dispatch(resetList());
};
return (
<div>
<div>List</div>
<button onClick={handleResetList}>Reset</button>
I'm using the Redux Thunk example template. When I dispatch an action in getInitialProps, that populates my store, the data is loaded but after the page is rendered, the store is still empty.
static async getInitialProps({ reduxStore }) {
await reduxStore.dispatch(fetchCategories())
const categories = reduxStore.getState().programm.categories;
console.log('STATE!!!', categories)
return { categories }
}
The categories will load correctly but when I inspect my store, the categories state is empty.
Here is my store:
import db from '../../api/db'
// TYPES
export const actionTypes = {
FETCH_PROGRAMMS: 'FETCH_PROGRAMMS',
FETCH_CATEGORIES: 'FETCH_CATEGORIES'
}
// ACTIONS
export const fetchCategories = () => async dispatch => {
const categories = await db.fetchCategories();
console.log('loaded Cate', categories)
return dispatch({
type: actionTypes.FETCH_CATEGORIES,
payload: categories
})
}
// REDUCERS
const initialState = {
programms: [],
categories: []
}
export const programmReducers = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PROGRAMMS:
return Object.assign({}, state, {
programms: action.payload
})
case actionTypes.FETCH_CATEGORIES:
console.log('Payload!', action);
return Object.assign({}, state, {
categories: action.payload
})
default:
return state
}
}
How can I make the redux state loaded on the server (in getInitialProps) be carried over to the client?
After hours of searching for solution it seems like I found my problem. It seems like I need to pass an initialState when creating the store. So instead of this:
export function initializeStore() {
return createStore(
rootReducers,
composeWithDevTools(applyMiddleware(...middleware))
)
}
I'm doing this and it works now
const exampleInitialState = {}
export function initializeStore(initialState = exampleInitialState) {
return createStore(
rootReducers,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
}
If you do this:
return { categories }
in getInitialProps, categories should be available in component's props in client side.
It should be available in Redux as well, this could cause the problem:
return Object.assign({}, state, {
categories: action.payload
})
Take a look at this Object.assign, the function only takes 2 parameters.
My normal way of doing this:
return {
...state,
categories: action.payload,
};
I have been trying to connect my Redux Action and Reducer to my component. But it doesn't seem to work properly.
Currently, when I call my Action, it does get to that Action but it does not move onto my reducer. I think I am missing something here but having a hard time finding out what is the issue.
Could anyone please help me with this issue?
Thank you.
Here is my Action:
export const getItem = () => {
return (dispatch, getState) => {
debugger;
dispatch({
type: 'API_REQUEST',
options: {
method: 'GET',
endpoint: `18.222.137.195:3000/v1/item?offset=0`,
actionTypes: {
success: types.GET_ITEM_SUCCESS,
loading: types.GET_ITEM_LOADING,
error: types.GET_ITEM_SUCCESS
}
}
});
};
};
Here is my Reducer:
export const initialState = {
getItem: {}
};
const registerItemReducer = (state = initialState, action) => {
switch (action.type) {
case types.GET_ITEM_LOADING:
debugger;
return { ...state, loading: true, data: null };
case types.GET_ITEM_SUCCESS:
debugger;
return { ...state, loading: false, getItem: action.data};
case types.GET_ITEM_ERROR:
debugger;
return { ...state, loading: false, error: action.data};
default: {
return state;
}
}
}
export default registerItemReducer;
Here is my store:
/* global window */
import { createStore, applyMiddleware, compose } from 'redux';
import { persistStore, persistCombineReducers } from 'redux-persist';
import storage from 'redux-persist/es/storage'; // default:
localStorage if web, AsyncStorage if react-native
import thunk from 'redux-thunk';
import reducers from '../reducers';
// Redux Persist config
const config = {
key: 'root',
storage,
blacklist: ['status'],
};
const reducer = persistCombineReducers(config, reducers);
const middleware = [thunk];
const configureStore = () => {
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
compose(applyMiddleware(...middleware)),
);
const persistor = persistStore(
store,
null,
() => { store.getState(); },
);
return { persistor, store };
};
export default configureStore;
Lastly here is my component that has "connect" part & componentDidMount:
componentDidMount() {
this.props.getItem();
}
const mapStateToProps = state => ({
registerItem: state.registerItem || {},
});
const mapDispatchToProps = {
getItem: getItem
};
export default connect(mapStateToProps, mapDispatchToProps)(RegisterItemComponent);
Is registerItem name of your reducer? Your reducer has two state getItem and loading. But in the below code you are calling state.registerItem. Looks like there is some mismatch between the actual state and the mapped state.
In the code below, try to print the state value, it will help you to navigate to the exact parameter you are looking for.
Add the below line in your existing code to debug:
const mapStateToProps = state => ({
console.log("State of reducer" + JSON.stringify(state));
registerItem: state.registerItem || {},
});