Nested redux reducers - javascript

Is it possible to combine reducers that are nested with the following structure:
import 'user' from ...
import 'organisation' from ...
import 'auth' from ...
// ...
export default combineReducers({
auth: {
combineReducers({
user,
organisation,
}),
auth,
},
posts,
pages,
widgets,
// .. more state here
});
Where the state has the structure:
{
auth: {
user: {
firstName: 'Foo',
lastName: 'bar',
}
organisation: {
name: 'Foo Bar Co.'
phone: '1800-123-123',
},
token: 123123123,
cypher: '256',
someKey: 123,
}
}
Where the auth reducer has the structure:
{
token: 123123123,
cypher: '256',
someKey: 123,
}
so maybe the spread operator is handy? ...auth not sure :-(

It is perfectly fine to combine your nested reducers using combineReducers. But there is another pattern which is really handy: nested reducers.
const initialState = {
user: null,
organisation: null,
token: null,
cypher: null,
someKey: null,
}
function authReducer(state = initialState, action) {
switch (action.type) {
case SET_ORGANISATION:
return {...state, organisation: organisationReducer(state.organisation, action)}
case SET_USER:
return {...state, user: userReducer(state.user, action)}
case SET_TOKEN:
return {...state, token: action.token}
default:
return state
}
}
In the above example, the authReducer can forward the action to organisationReducer and userReducer to update some part of its state.

Just wanted to elaborate a bit on the very good answer #Florent gave and point out that you can also structure your app a bit differently to achieve nested reducers, by having your root reducer be combined from reducers that are also combined reducers
For example
// src/reducers/index.js
import { combineReducers } from "redux";
import auth from "./auth";
import posts from "./posts";
import pages from "./pages";
import widgets from "./widgets";
export default combineReducers({
auth,
posts,
pages,
widgets
});
// src/reducers/auth/index.js
// note src/reducers/auth is instead a directory
import { combineReducers } from "redux";
import organization from "./organization";
import user from "./user";
import security from "./security";
export default combineReducers({
user,
organization,
security
});
this assumes a bit different of a state structure. Instead, like so:
{
auth: {
user: {
firstName: 'Foo',
lastName: 'bar',
}
organisation: {
name: 'Foo Bar Co.'
phone: '1800-123-123',
},
security: {
token: 123123123,
cypher: '256',
someKey: 123
}
},
...
}
#Florent's approach would likely be better if you're unable to change the state structure, however

Inspired by #florent's answer, I found that you could also try this. Not necessarily better than his answer, but i think it's a bit more elegant.
function userReducer(state={}, action) {
switch (action.type) {
case SET_USERNAME:
state.name = action.name;
return state;
default:
return state;
}
}
function authReducer(state = {
token: null,
cypher: null,
someKey: null,
}, action) {
switch (action.type) {
case SET_TOKEN:
return {...state, token: action.token}
default:
// note: since state doesn't have "user",
// so it will return undefined when you access it.
// this will allow you to use default value from actually reducer.
return {...state, user: userReducer(state.user, action)}
}
}

Example (see attachNestedReducers bellow)
import { attachNestedReducers } from './utils'
import { profileReducer } from './profile.reducer'
const initialState = { some: 'state' }
const userReducerFn = (state = initialState, action) => {
switch (action.type) {
default:
return state
}
}
export const userReducer = attachNestedReducers(userReducerFn, {
profile: profileReducer,
})
State object
{
some: 'state',
profile: { /* ... */ }
}
Here is the function
export function attachNestedReducers(original, reducers) {
const nestedReducerKeys = Object.keys(reducers)
return function combination(state, action) {
const nextState = original(state, action)
let hasChanged = false
const nestedState = {}
for (let i = 0; i < nestedReducerKeys.length; i++) {
const key = nestedReducerKeys[i]
const reducer = reducers[key]
const previousStateForKey = nextState[key]
const nextStateForKey = reducer(previousStateForKey, action)
nestedState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? Object.assign({}, nextState, nestedState) : nextState
}
}

Nested Reducers Example:
import {combineReducers} from 'redux';
export default combineReducers({
[PATH_USER_STATE]: UserReducer,
[PATH_CART_STATE]: combineReducers({
[TOGGLE_CART_DROPDOWN_STATE]: CartDropdownVisibilityReducer,
[CART_ITEMS_STATE]: CartItemsUpdateReducer
})
});
Output:
{
cart: {toggleCartDropdown: {…}, cartItems: {…}}
user: {currentUser: null}
}

Related

How do I export a redux reducer that uses a prepare function

I am currently working on a anecdotes application, and anytime a user votes on a certain anecdote, it should display a notification then disappear 10 seconds later, however, I am struggling to export my reducer that uses the prepare function that is suppose to, I think, get my multiple arguments ready for the actual reducer. Here is my code in question:
import { createSlice } from "#reduxjs/toolkit";
const notificationSlice = createSlice({
name: 'notification',
initialState: '',
reducers: {
test: {
createNotification(state, action) {
console.log(action)
},
prepare(...args) {
return {
payload: args
}
}
}
}})
export const { createNotification } = notificationSlice.actions
export default notificationSlice.reducer
I thought I could just export const { test.createNotification } = notificationSlice.actions but that does not work due to the dot in the variable name.
How would I then export my createNotification reducer since test is the first property of the reducers object?
I found the solution, I just name the reducer createNotification with two key values of reducer and prepare
import { createSlice } from "#reduxjs/toolkit";
const notificationSlice = createSlice({
name: 'notification',
initialState: '',
reducers: {
createNotification: {
reducer(state, action) {
console.log(action)
},
prepare(...args) {
return {
payload: args
}
}
}
}})
export const { createNotification } = notificationSlice.actions
export default notificationSlice.reducer

Reducer is not a function. React Native Redux

I am trying to add a reducer to my react native app.
Here is the store constant:
export const USER_PROFILE = 'USER_PROFILE';
Here is the action.js
import {USER_PROFILE} from '../constants/index';
export function userProfile(userReducer) {
return {
type: USER_PROFILE,
payload: {
email: '',
},
};
}
This the userReducer that is causing the error. I keep getting an error of customerReducer is not a function.
import {USER_PROFILE} from '../constants/index';
const initialState = {
userProfile: '',
};
const customerReducer = (state = initialState, action) => {
switch (action.type) {
case USER_PROFILE:
return {
...state,
userProfile: action.payload,
};
default:
return state;
}
};
export default customerReducer;
And I have declared the email as a state... const [email, setEmail] useState('')
calling the reducer here. const customerReducer = useSelector(state => state.userProfile);
Now dispatching it with the useDispatch method.
const dispatch = useDispatch();
...
dispatch(customerReducer(email));
Your action.js should look somthing like this:
import {USER_PROFILE} from '../constants/index';
export function userProfile(userReducer) {
return {
type: USER_PROFILE,
payload: {
email: userReducer.email,
},
};
and when you dispatch you should use the action "userProfile" not the reducer "customerReducer". Your dispatch should look somthing like this:
dispatch(userProfile({email}));
make sure to use curly brackets. beacause you should pass an object not a string.

Redux-Toolkit Store does not have a valid reducer

I'm using Redux-Toolkit for the first time, followed the Quick-start in their documentation and am met with this error
Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
I logged the reducers and the store objects to console and found that the store object from configure store doesn't have a reducer property at all.
If any of you can help out, I'd really appreciate it.
P.S : I already looked at all the other stackoverflow question on this. Nothing is relevant.
Here's my code
store.js
import { configureStore } from "#reduxjs/toolkit";
// Redux Reducers
import { authSlice } from "./slices/authSlice";
import { projectSlice } from "./slices/projectSlice";
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
project: projectSlice.reducer,
},
});
authSlice.js
import { createSlice } from "#reduxjs/toolkit";
export const authSlice = createSlice({
name: "auth",
initialState: {
isUserLoggedIn: false,
userDetails: {},
},
reducers: {
login: (state) => {
state.auth.isUserLoggedIn = true;
},
logout: (state) => {
state.auth.isUserLoggedIn = false;
},
register: (state) => {
state.auth.isUserLoggedIn = true;
},
},
});
export const { login, logout, register } = authSlice.actions;

Redux store property gets nested into itself on update

I'm having this issue where my props are ending up looking like this (on console.log):
{
fetchRoles: f(),
roles:
roles: ["Admin", "Manager"],
}
As you can see, somewhere in my code I'm making a mistake that causes the roles prop to get nested into itself, which would force me into doing const { roles } = this.props.roles; in order to retrieve my data (which works BTW).
I've looked around for help but not many people seem to have run into this issue (I'm just getting started with redux).
Below you can see my files:
rolesReducer.js:
import { FETCH_ROLES } from "../actions/types";
const initialState = {
roles: [],
};
export default function (state = initialState, action) {
const { roles } = action;
switch (action.type) {
case FETCH_ROLES:
return {
...state, //also tried ...state.roles and same issue.
roles,
};
default:
return state;
}
}
rolesActions.js:
import { FETCH_ROLES } from "./types";
const roles = ["SuperAdmin"];
export function fetchRoles() {
return function (dispatch) {
dispatch({
type: FETCH_ROLES,
roles,
});
};
}
reducers/index.js (root reducer):
import { combineReducers } from "redux";
import rolesReducer from "./rolesReducer";
import roleMembersReducer from "./roleMembersReducer";
export default combineReducers({
roles: rolesReducer,
roleMembers: roleMembersReducer,
});
PermissionsManager.jsx:
import React, { Component } from "react";
import { connect } from "react-redux";
import { Container } from "react-bootstrap";
import { fetchRoles } from "../redux/actions/rolesActions";
import RoleContainer from "./RoleContainer";
class PermissionsManager extends Component {
componentDidMount() {
this.props.fetchRoles();
}
render() {
console.log(this.props);
const { roles } = this.props.roles;
return (
<Container>
{roles.map((role) => {
return <RoleContainer key={role} role={role} />;
})}
</Container>
);
}
}
const mapStateToProps = (state) => {
return {
roles: state.roles,
};
};
export default connect(mapStateToProps, { fetchRoles })(PermissionsManager);
Edit 1 - Adding reducer log:
As suggested, I logged the reducer, specifically state and action:
state:
{
roles: [],
}
action:
{
roles: ["Admin", "Manager"],
type: "FETCH_ROLES",
}
No duplication or abnormal structures I believe.
One way to shape the store like you're asking is to flatten roles in rolesReducer.js,
you can do so storing the received array directly in the partial state:
initialState would need to look like
const initialState = []
and in the switch statement
case FETCH_ROLES:
return roles

CombineReducers in TypeScript

export const rootReducer = combineReducers({
login: loginReducer,
});
This works fine but as soon as I try to combine another reducer,
export const rootReducer = combineReducers({
login: loginReducer,
logout: logoutReducer
});
I start getting an error on rootReducer that
'rootReducer' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
How could I modify this?
This is how my logoutReducer looks like:
import {store} from '../index'
export const logoutReducer = (state = store.getState(), { type, payload } :any) => {
switch (type) {
case "logout":
return {...state, token: payload};
default:
return state;
}
};
Have you tried this with assigning type to your underlying reducers?
For example:
import {Action, Reducer} from 'redux';
interface LoginState {
isLoggedIn: boolean;
token: string;
}
interface LogoutState {
token:string;
}
export const logOutReducer: Reducer<LogoutState> = (state: LogoutState | undefined, incomingAction: Action): LogoutState=> {
switch (incomingAction.type) {
case "logout":
return {...state, token: payload};
default:
return state;
}
}
//... export const logInReducer: Reducer<LoginState>...

Categories

Resources