Call sub-reducer in default block of a parent reducer - redux

Is it ok to have a reducer calling sub-reducers in its default block?
function aReducer(state = {}, action) {
switch(action.type) {
case XYZ:
... // know what to do
default:
// don't know this action, let's delegate to the children
return {
sub1: subReducer1(state.sub1, action),
sub2: subReducer2(state.sub2, action)
}
}
}

Yes, that's absolutely legal and reasonable to do.
You might want to read through the Redux docs section on "Structuring Reducers" for further ideas on how you can organize reducer logic as well.

You can put all the reducers in a common folder and inside that, you can combine the separate reducers into a single one like the following code.
import { combineReducers } from 'redux'
import Reducer1 from './Reducer1.js'
import Reducer2 from './Reducer2.js'
export default combineReducers( { Reducer1, Reducer2,... } )
and use the following code to use that as a single reducer.
import reducers from '../../reducers'(reducer's root folder name/path)
let store = createStore( reducers );

Related

Rewrite redux-orm reducer with redux-toolkit

Issue (tl;dr)
How can we create a custom redux-orm reducer with redux-toolkit's createSlice?
Is there a simpler, recommended, more elegant or just other solution than the attempt provided in this question?
Details
The example of a custom redux-orm reducer looks as follows (simplified):
function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case 'CREATE_BOOK':
Book.create(action.payload);
break;
case 'REMOVE_AUTHOR_FROM_BOOK':
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case 'ASSIGN_PUBLISHER':
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
It's possible to simplify reducers with the createSlice function of redux-toolkit (based on the redux-toolkit usage-guide):
const ormSlice = createSlice({
name: 'orm',
initialState: [],
reducers: {
createBook(state, action) {},
removeAuthorFromBook(state, action) {},
assignPublisher(state, action) {}
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
However, at the beginning of redux-orm reducer we need to create a session
const session = orm.session(dbState);
then we do our redux-orm reducer magic, and at the end we need to return the state
return session.state;
So we miss something like beforeEachReducer and afterEachReducer methods in the createSlice to add this functionality.
Solution (attempt)
We created a withSession higher-order function that creates the session and returns the new state.
const withSession = reducer => (state, action) => {
const session = orm.session(state);
reducer(session, action);
return session.state;
}
We need to wrap every reducer logic in this withSession.
import { createSlice } from '#reduxjs/toolkit';
import orm from './models/orm'; // defined elsewhere
// also define or import withSession here
const ormSlice = createSlice({
name: 'orm',
initialState: orm.session().state, // we need to provide the initial state
reducers: {
createBook: withSession((session, action) => {
session.Book.create(action.payload);
}),
removeAuthorFromBook: withSession((session, action) => {
session.Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
}),
assignPublisher: withSession((session, action) => {
session.Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}),
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
This is a fascinating question for me, because I created Redux Toolkit, and I wrote extensively about using Redux-ORM in my "Practical Redux" tutorial series.
Off the top of my head, I'd have to say your withSession() wrapper looks like the best approach for now.
At the same time, I'm not sure that using Redux-ORM and createSlice() together really gets you a lot of benefit. You're not making use of Immer's immutable update capabilities inside, since Redux-ORM is handling updates within the models. The only real benefit in this case is generating the action creators and action types.
You might be better off just calling createAction() separately, and using the original reducer form with the generated action types in the switch statement:
export const createBook = createAction("books/create");
export const removeAuthorFromBook = createAction("books/removeAuthor");
export const assignPublisher = createAction("books/assignPublisher");
export default function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case createBook.type:
Book.create(action.payload);
break;
case removeAuthorFromBook.type:
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case assignPublisher.type:
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
I see what you're saying about adding some kind of "before/after" handlers, but that would add too much complexity. RTK is intended to handle the 80% use case, and the TS types for createSlice are already incredibly complicated. Adding any more complexity here would be bad.
I came across this question looking to combine the benefits of redux-toolkit
and redux-orm. I was able to come up with a solution I've been pretty happy
with so far. Here is what my redux-orm model looks like:
class Book extends Model {
static modelName = 'Book';
// Declare your related fields.
static fields = {
id: attr(), // non-relational field for any value; optional but highly recommended
name: attr(),
// foreign key field
publisherId: fk({
to: 'Publisher',
as: 'publisher',
relatedName: 'books',
}),
authors: many('Author', 'books'),
};
static slice = createSlice({
name: 'BookSlice',
// The "state" (Book) is coming from the redux-orm reducer, and so will
// never be undefined; therefore, `initialState` is not needed.
initialState: undefined,
reducers: {
createBook(Book, action) {
Book.create(action.payload);
},
removeAuthorFromBook(Book, action) {
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
},
assignPublisher(Book, action) {
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}
}
});
toString() {
return `Book: ${this.name}`;
}
// Declare any static or instance methods you need.
}
export default Book;
export const { createBook, removeAuthorFromBook, assignPublisher } = Book.slice.actions;
The redux-toolkit slice is created as a static property on the class, and then
the model and its actions are exported in a manner similar to Ducks
(ORMDucks??).
The only other modification to make is to define a custom updater for
redux-orm's reducer:
const ormReducer = createReducer(orm, function (session, action) {
session.sessionBoundModels.forEach(modelClass => {
if (typeof modelClass.slice.reducer === 'function') {
modelClass.slice.reducer(modelClass, action, session);
}
});
});
See a more complete example here:
https://gist.github.com/JoshuaCWebDeveloper/25a302ec891acb6c4992fe137736160f
Some Notes
#markerikson makes a good point about some of the features of redux-toolkit
not being used since redux-orm is managing the state. For me, the two
greatest benefits of using this method are not having to wrangle a whole
bunch of action creators and not having to contend with awful switch
statements :D.
I am using the stage 3 class fields and static class features proposals. (See
https://babeljs.io/docs/en/babel-plugin-proposal-class-properties). To make
this ES6 compatible, you can easily refactor the model class to define its
static props using the current syntax (i.e. Book.modelName = 'Book';).
If you decide to mix models like the one above with models that don't define
a slice, then you'll need to tweak the logic in the createReducer updater
slightly.
For a real world example, see how I use the model in my project here:
https://github.com/vallerance/react-orcus/blob/70a389000b6cb4a00793b723a25cac52f6da519b/src/redux/models/OrcusApp.js.
This project is still in the early stages. The largest question in my mind is
how well this method will scale; however, I am optimistic that it will continue
to provide numerous benefits as my project matures.
Try using normalized-reducer. It's a higher-order-reducer that takes a schema describing the relationships, and returns a reducer, action, and selectors that write/read according to the relationships.
It also integrates easily with Normalizr and Redux Toolkit.

How to return a map of reducers in getReducers(): ActionReducerMap<fromFeature.State>?

I'm trying to inject feature reducers when composing state through NgRx feature modules.
import { NgModule, InjectionToken } from '#angular/core';
import { StoreModule, ActionReducerMap } from '#ngrx/store';
import * as fromFeature from './reducers';
export const FEATURE_REDUCER_TOKEN = new InjectionToken<ActionReducerMap<fromFeature.State>>('Feature Reducers');
What I am supposed to return here?
export function getReducers(): ActionReducerMap<fromFeature.State> {
// map of reducers
return {
};
}
I tried
export function getReducers(): ActionReducerMap<fromFeature.State> {
// map of reducers
return {
reducerA: FeatureAReducer
};
}
But it gives the error Object literal may only specify known properties.
The rest of module code:
#NgModule({
imports: [
StoreModule.forFeature('feature', FEATURE_REDUCER_TOKEN),
],
providers: [
{
provide: FEATURE_REDUCER_TOKEN,
useFactory: getReducers
}
]
})
export class FeatureModule { }
I thought that each reducer, whether it is root or feature reducer, returns a new state object. But actually it doesn't. What feature reducer does, it returns only the segment of the state which it updates.
From the ngrx/platform/example-app:
we treat each reducer like a table in a database. This means our
top level state interface is just a map of keys to inner state types.

How to organize Redux Action-Creators for use in Redux-React

I'm using react-redux & redux-thunk for my project.
I have to inject my actions to a component by using connect.
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
My task is one level up. I don't want just to inject multiple actions in this form:
{
doThis(),
doThat()
}
But in this form:
{
this: {
doThis1(),
doThis2()
}
that: {
doThat()
}
}
So basically my problem is that I want to dispatch multiple action-creator files because I want them organized as such.
I tried this version which obviously doesn't work because dispatch is not injected in each Thunk Action Creator:
import * as actions from './actions'
const mapDispatchToProps = (dispatch) => {
return {
dataActions: {
...actions.dataActions
}
};
}
export default connect(null, mapDispatchToProps)(Component);
So my final question is:
Am I even supposed to use Redux this way? Can I organize my files this way, if so how?
If instead of having one property per action creator, you want to structure your bound action creators in a couple of properties that each contain a group of action creators, you can do something like this:
import { bindActionCreators, .. } from 'redux';
..
const mapDispatchToProps = (dispatch) => {
return {
dataActions: bindActionCreators(actions.dataActions, dispatch),
otherActions: bindActionCreators(actions.otherActions, dispatch),
..
};
};
The first argument to bindActionCreators is an object containing action-creator functions (e.g. an imported module that exports only action creators). In your actual component, you should then be able to use this.props.dataActions.someDataAction(..).
If the question is just about whether you can keep different action creators in different files, you might not even want to group the action creators and just do this:
return {
...bindActionCreators(actionCreatorsFromOneModule, dispatch),
...bindActionCreators(actionCreatorsFromAnotherModule, dispatch),
..
};

React and Redux: redirect after action

I develop a website with React/Redux and I use a thunk middleware to call my API. My problem concerns redirections after actions.
I really do not know how and where I can do the redirection: in my action, in the reducer, in my component, … ?
My action looks like this:
export function deleteItem(id) {
return {
[CALL_API]: {
endpoint: `item/${id}`,
method: 'DELETE',
types: [DELETE_ITEM_REQUEST, DELETE_ITEM_SUCCESS, DELETE_ITEM_FAILURE]
},
id
};
}
react-redux is already implemented on my website and I know that I can do as below, but I do not want to redirect the use if the request failed:
router.push('/items');
Thanks!
Definitely do not redirect from your reducers since they should be side effect free.
It looks like you're using api-redux-middleware, which I believe does not have a success/failure/completion callback, which I think would be a pretty useful feature for the library.
In this question from the middleware's repo, the repo owner suggests something like this:
// Assuming you are using react-router version < 4.0
import { browserHistory } from 'react-router';
export function deleteItem(id) {
return {
[CALL_API]: {
endpoint: `item/${id}`,
method: 'DELETE',
types: [
DELETE_ITEM_REQUEST,
{
type: DELETE_ITEM_SUCCESS,
payload: (action, state, res) => {
return res.json().then(json => {
browserHistory.push('/your-route');
return json;
});
},
},
DELETE_ITEM_FAILURE
]
},
id
}
};
I personally prefer to have a flag in my connected component's props that if true, would route to the page that I want. I would set up the componentWillReceiveProps like so:
componentWillReceiveProps(nextProps) {
if (nextProps.foo.isDeleted) {
this.props.router.push('/your-route');
}
}
The simplest solution
You can use react-router-dom version *5+ it is actually built on top of react-router core.
Usage:
You need to import useHistory hook from react-router-dom and directly pass it in your actions creator function call.
Import and Creating object
import { useHistory } from "react-router-dom";
const history = useHistory();
Action Call in Component:
dispatch(actionName(data, history));
Action Creator
Now, you can access history object as a function argument in action creator.
function actionName(data, history) {}
Usually the better practice is to redirect in the component like this:
render(){
if(requestFullfilled){
router.push('/item')
}
else{
return(
<MyComponent />
)
}
}
In the Redux scope must be used react-redux-router push action, instead of browserHistory.push
import { push } from 'react-router-redux'
store.dispatch(push('/your-route'))
I would love not to redirect but just change the state. You may just omit the result of deleted item id:
// dispatch an action after item is deleted
dispatch({ type: ITEM_DELETED, payload: id })
// reducer
case ITEM_DELETED:
return { items: state.items.filter((_, i) => i !== action.payload) }
Another way is to create a redirect function that takes in your redirect Url:
const redirect = redirectUrl => {
window.location = redirectUrl;
};
Then import it into your action after dispatch and return it;
return redirect('/the-url-you-want-to-redirect-to');
import { useHistory } from "react-router-dom";
import {useEffect} from 'react';
const history = useHistory();
useEffect(()=>{
// change if the user has successfully logged in and redirect to home
//screen
if(state.isLoggedIn){
history.push('/home');
}
// add this in order to listen to state/store changes in the UI
}, [state.isLoggedIn])
A simple solution would be to check for a state change in componentDidUpdate. Once your state has updated as a result of a successful redux action, you can compare the new props to the old and redirect if needed.
For example, you dispatch an action changing the value of itemOfInterest in your redux state. Your component receives the new state in its props, and you compare the new value to the old. If they are not equal, push the new route.
componentDidUpdate(prevProps: ComponentProps) {
if (this.props.itemOfInterest !== prevProps.itemOfInterest) {
this.props.history.push('/');
}
}
In your case of deleting an item, if the items are stored in an array, you can compare the new array to the old.

Redux Newbie Trying to Make Pure-Data Class That Uses Redux Store

Basic Problem
I've got a Redux store with the following data:
foo: {
currentId: 1,
things: [{id: 1}, {id: 2}),
}
I'd like to make a utility method somewhere (eg. on a Foo singleton object) such that any module in my code can do:
import Foo from 'foo';
foo.getCurrentFoo(); // returns foo.thins[foo.currentId];
but I'm having trouble figuring out where to put it.
Failed Attempt
My initial attempt was to create a Foo component singleton:
// Foo.js
class FooBase extends React.Component {
getCurrentFoo() {
return this.state.foo.things[this.state.foo.currentId];
}
}
const Foor = connect((state) => state.foo)(FooBase);
export default new FooWrapper();
But that doesn't work. Redux complaieds about the property store not existing (when I did new FooWrapper()). That makes sense, because my component isn't inside a <Provider />. However, I just want a stand-alone utility class/object, not something actually in the DOM, which rules out <Provider/>.
How can I make a method like the one described above, that actually works, without involving <Provider /> ... and where do I put it?
The nice thing about the react-redux helpers is that they allow you to use connect() and <Provider /> to automatically pass the store to child components via React's context. However, that doesn't necessarily mean that you have to use these helpers, especially in areas of a codebase that don't use React.
So here lies the problem: connect() and <Provider /> help us by giving our React components access to a singleton instance of a store, but how can we access this store somewhere where connect() and <Provider /> can't be used?
I think the easiest solution here is to create a singleton class that holds on to the store, so any non-React module can still use the store.
So let's say you're creating your store like this:
init.js
import {createStore} from 'redux';
const initialState = {
currentId: 1,
things: ['foo', 'bar']
};
const reducer = (state = initialState, action) => {
if (action.type === 'SET_CURRENT_ID') {
return Object.assign({}, state, {
currentId: action.id
});
}
return state;
};
const store = createStore(reducer);
This store takes an action of type SET_CURRENT_ID which simply returns a new state with the currentId property changed to whatever was handed to it. You could then get the current "thing" by doing something like store.getState().things[store.getState().currentId]. So let's create a Singleton class that can hold on to the store and provide a wrapper around this functionality.
store.js
class Store {
constructor() {
this._store = undefined;
}
setStore(store) {
this._store = store;
}
getCurrentThing() {
if (this._store) {
const {things, currentId} = this._store.getState();
return things[currentId];
}
}
setCurrentThing(id) {
if (this._store) {
const action = {
type: 'SET_CURRENT_ID',
id
};
this._store.dispatch(action);
}
}
}
export let singletonStore = new Store();
This class creates an instance the first time it is used, and uses that instance every subsequent time. So when you originally create your store, simply import this class and call setStore().
init.js
import {singletonStore} from './store';
singletonStore.setStore(store);
Then, every subsequent file where singletonStore is used will have the same state.
test.js
import {singletonStore} from './store';
console.log(singletonStore.getCurrentThing()); // 'bar'
singletonStore.setCurrentThing(0);
console.log(singletonStore.getCurrentThing()); // 'foo'
This should work just fine for your need to use your store in modules that don't have the benefit of being passed the store magically with connect() and <Provider />.

Resources