Accessing a reducer state from within another reducer in NGRX - ngrx

How can i access (read) the state of a reducer state within a another reducer in NGRX?
It is a very similar question to this.
Does NGRX offers any other solution to this?

I just stumbled upon this question when thinking about doing something similar. I needed a copy of some state information from a different reducer as a temporary "editing" data set that could be canceled and reverted back to the "recorded" data set.
I ended up extending my UI reducer (the reducer that held the state/edits for the current user's session) to include a property to hold a copy of data from my Data reducer (the reducer that represents what is stored in the database). That data gets passed in via a "Start" action.
Here is an abbreviated copy of my UI action:
import { Action } from "#ngrx/store";
import {
ICableFeature
} from "../../shared/models";
export enum UIActionTypes {
...
UI_EDIT_CUSTOM_TAGS_START = "[UI] Edit Custom Tags Start",
UI_EDIT_CUSTOM_TAGS_UPDATE = "[UI] Edit Custom Tags Update",
UI_EDIT_CUSTOM_TAGS_SAVE = "[UI] Edit Custom Tags Save",
UI_EDIT_CUSTOM_TAGS_SUCCESSFUL = "[UI] Edit Custom Tags Successful",
UI_EDIT_CUSTOM_TAGS_FAILED = "[UI] Edit Custom Tags Failed",
UI_EDIT_CUSTOM_TAGS_CANCELED = "[UI] Edit Custom Tags Canceled"
}
...
export class EditCustomTagsStart implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_START;
constructor(public payload: ICableFeature) {}
}
export class EditCustomTagsUpdate implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_UPDATE;
constructor(public payload: ICableFeature) {} // This payload has a copy of the data I needed from the other reducer. Make sure it is a copy and not the same object!
}
export class EditCustomTagsSave implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_SAVE;
constructor() {}
}
export class EditCustomTagsSuccessful implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_SUCCESSFUL;
constructor() {}
}
export class EditCustomTagsFailed implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_FAILED;
constructor(public payload: string) {}
}
export class EditCustomTagsCanceled implements Action {
readonly type = UIActionTypes.UI_EDIT_CUSTOM_TAGS_CANCELED;
constructor() {}
}
export type UIActions =
...
| EditCustomTagsStart
| EditCustomTagsUpdate
| EditCustomTagsSuccessful
| EditCustomTagsFailed
| EditCustomTagsCanceled;
And here is an abbreviated copy of my UI reducer:
import * as fromUI from "../actions/ui.actions";
import {
IOrchestratorState,
IOrchestratorStatusState,
ICableFeature
} from "../../shared/models";
export const uiFeatureKey = "ui";
export interface State {
showScroll: boolean;
fillingGaps: boolean;
fillGapStatus: IOrchestratorStatusState;
currentFillGapState: IOrchestratorState;
editingCustomTags: boolean;
customTagEdits: ICableFeature;
}
export const initialState: State = {
showScroll: true,
fillingGaps: false,
fillGapStatus: null,
currentFillGapState: null,
editingCustomTags: false,
customTagEdits: null
};
export function reducer(state = initialState, action: fromUI.UIActions) {
switch (action.type) {
...
case fromUI.UIActionTypes.UI_EDIT_CUSTOM_TAGS_START:
return {
...state,
editingCustomTags: true,
customTagEdits: action.payload // This is a copy of the data I needed from the other reducer
};
case fromUI.UIActionTypes.UI_EDIT_CUSTOM_TAGS_UPDATE:
return {
...state,
customTagEdits: action.payload // This is the updated information from user edits, not saved.
// I can also create a router guard that checks to makes sure the data in this
// property and the data in my Data store are the same before the page is deactivated
};
case fromUI.UIActionTypes.UI_EDIT_CUSTOM_TAGS_SUCCESSFUL:
case fromUI.UIActionTypes.UI_EDIT_CUSTOM_TAGS_FAILED:
case fromUI.UIActionTypes.UI_EDIT_CUSTOM_TAGS_CANCELED:
return {
...state,
editingCustomTags: false,
customTagEdits: null
};
default:
return state;
}
}
As you can see, by passing the data from one reducer to the other as payloads in actions, I am able to maintain pure functions in my reducers.
I hope this helps!

Related

How to listen for global action and update the rootState directly, while using multiple slices with Redux Toolkit?

I've got a state in my Redux store shaped like this:
type RootState = {
PAGE_A: StatePageA,
PAGE_B: StatePageB,
PAGE_C: StatePageC,
// AND SO ON...
}
Each page is a slice created with createSlice from #reduxjs/toolkit
I'm using Next.js static generation and SSR, so I need to hydrate my previously created store with the new state that comes form subsequent pages from the Next.js server.
Whatever page I'm routing to, I'll get the pre-rendered pageState, which should by of type StatePageA | StatePageB | StatePageC.
When that new state comes, I need to dispatch() a HYDRATE_PAGE action, which should replace the current page state.
This was my first idea on how to handle the HYDRATE_PAGE action.
export const HYDRATE_PAGE = createAction<{
pageName: "PAGE_A" | "PAGE_B" | "PAGE_C",
pageState: StatePageA | StatePageB | StatePageC
}>("HYDRATE_PAGE");
Then I would have to listen for it in every slice:
// EX: PAGE_A SLICE
// THIS WOULD HAVE TO BE DONE FOR EACH SLICE ON THE extraReducers PROPERTY
extraReducers: {
[HYDRATE_PAGE]: (state, action) => {
if (action.payload.pageName === "PAGE_A")
return action.payload.pageState
}
}
Is there a way where I can handle it in a centralized manner, without having to listen for that action in every slice reducer?
For example: a rootReducer kind of thing where I could listen for the HYDRATE_PAGE action and handle it in a single place like.
// THIS REDUCER WOULD HAVE ACCESS TO THE rootState
(state: RootState, action) => {
const {pageName, pageState} = action.payload;
state[pageName] = pageState;
};
Is this possible somehow?
Extra
There is this package called next-redux-wrapper, but I don't intent to use it. I'm trying to build my custom solution.
Overview of Concept
I'm not sure that your store has the best design because generally you want data to be organized by what type of data it is rather than what page it's used on. But I'm just going to answer the question as it's presented here.
A reducer is simply a function that takes a state and an action and returns the next state. When you combine a keyed object of slice reducers using combineReducers (or configureStore, which uses combineReducers internally), you get a function that looks like:
(state: RootState | undefined, action: AnyAction) => RootState
We want to create a wrapper around that function and add an extra step that performs the HYDRATE_PAGE action.
Typing the Action
export const HYDRATE_PAGE = createAction<{
pageName: "PAGE_A" | "PAGE_B" | "PAGE_C",
pageState: StatePageA | StatePageB | StatePageC
}>("HYDRATE_PAGE");
The typescript types that you have in your action creator aren't as good as they can be because we want to enforce that the pageName and pageState match each other. The type that we actually want is this:
type HydratePayload = {
[K in keyof RootState]: {
pageName: K;
pageState: RootState[K];
}
}[keyof RootState];
export const HYDRATE_PAGE = createAction<HydratePayload>("HYDRATE_PAGE");
Which evaluates to a union of valid pairings:
type HydratePayload = {
pageName: "PAGE_A";
pageState: StatePageA;
} | {
pageName: "PAGE_B";
pageState: StatePageB;
} | {
pageName: "PAGE_C";
pageState: StatePageC;
}
Modifying the Reducer
First we will create the normal rootReducer with combineReducers. Then we create the hydratedReducer as a wrapper around it. We call rootReducer(state, action) to get the next state. Most of the time we just return that state and don't do anything to it.
But if the action matches our HYDRATE_PAGE action then we modify the state by replacing the state for that slice with the one from our payload.
const rootReducer = combineReducers({
PAGE_A: pageAReducer,
PAGE_B: pageBReducer,
PAGE_C: pageCReducer
});
const hydratedReducer = (state: RootState | undefined, action: AnyAction): RootState => {
// call the reducer normally
const nextState = rootReducer(state, action);
// modify the next state when the action is hydrate
if (HYDRATE_PAGE.match(action)) {
const { pageName, pageState } = action.payload;
return {
...nextState,
[pageName]: pageState
};
}
// otherwise don't do anything
return nextState;
}
const store = configureStore({
reducer: hydratedReducer,
});

nested route using #ApiParam, how to use a custom validator in nestjs?

I’m looking for some help about custom validator & custom decorator in Nest.
FIRST CASE : working one
A DTO, with class-validator anotations :
import { IsNotEmpty, IsString } from 'class-validator';
import { IsOwnerExisting } from '../decorators/is-owner-existing.decorator';
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#IsOwnerExisting() // custom decorator, calling custom validator, using a service to check in db
ownerEmail: string;
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
I use it in a controller :
#Controller()
#ApiTags('/polls')
export class PollsController {
constructor(private readonly pollsService: PollsService) {}
#Post()
public async create(#Body() createPollDto: CreatePollDto): Promise<Poll> {
return await this.pollsService.create(createPollDto);
}
}
When this endpoint is called, the dto is validating by class-validator, and my custom validator works. If the email doesn’t fit any user in database, a default message is displayed.
That is how I understand it.
SECOND CASE : how to make it work ?
Now, I want to do something similar but in a nested route, with an ApiParam. I’d like to check with a custom validator if the param matches some object in database.
In that case, I can’t use a decorator in the dto, because the dto doesn’t handle the "slug" property, it’s a ManyToOne, and the property is on the other side.
// ENTITIES
export class Choice {
#ManyToOne((type) => Poll)
poll: Poll;
}
export class Poll {
#Column({ unique: true })
slug: string;
#OneToMany((type) => Choice, (choice) => choice.poll, { cascade: true, eager: true })
#JoinColumn()
choices?: Choice[];
}
// DTOs
export class CreateChoiceDto {
#IsNotEmpty()
#IsString()
label: string;
#IsOptional()
#IsString()
imageUrl?: string;
}
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
#IsOptional()
#IsArray()
#ValidateNested({ each: true })
#Type(() => CreateChoiceDto)
choices: CreateChoiceDto[] = [];
}
So where should I hook my validation ?
I’d like to use some decorator directly in the controller. Maybe it’s not the good place, I don’t know. I could do it in the service too.
#Controller()
#ApiTags('/polls/{slug}/choices')
export class ChoicesController {
constructor(private readonly choicesService: ChoicesService) {}
#Post()
#ApiParam({ name: 'slug', type: String })
async create(#Param('slug') slug: string, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
}
As in my first case, I’d like to use something like following, but in the create method of the controller.
#ValidatorConstraint({ async: true })
export class IsSlugMatchingAnyExistingPollConstraint implements ValidatorConstraintInterface {
constructor(#Inject(forwardRef(() => PollsService)) private readonly pollsService: PollsService) {}
public async validate(slug: string, args: ValidationArguments): Promise<boolean> {
return (await this.pollsService.findBySlug(slug)) ? true : false;
}
public defaultMessage(args: ValidationArguments): string {
return `No poll exists with this slug : $value. Use an existing slug, or register one.`;
}
}
Do you understand what I want to do ? Is it feasible ? What is the good way ?
Thanks a lot !
If you're needing to validate the slug with your custom rules you have one of two options
make a custom pipe that doesn't use class-validator and does the validation directly in it.
Use #Param() { slug }: CreatePollDto. This assumes that everything will be sent via URL parameters. You could always make the DTO a simple one such as
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
And then use #Param() { slug }: SlugDto, and now Nest will do the validation via the ValidationPipe for you.
If it didn't work with you with service try to use
getConnection().createQueryBuilder().select().from().where()
I used it in custom decorator to make a isUnique and it works well, but niot with injectable service.
public async validate(slug: string, args: ValidationArguments): Promise<boolean> { return (await getConnection().createQueryBuilder().select(PollsEntityAlias).from(PollsEntity).where('PollsEntity.slug =:slug',{slug}))) ? true : false; }
That’s so greeat! Thanks a lot, it’s working.
I’ve tried something like that, but can’t find the good way.
The deconstructed { slug }: SlugDto, so tricky & clever ! I’ve tried slug : SlugDto, but it couldn’t work, I was like «..hmmm… how to do that… »
Just something else : in the controller method, I was using (as in documentation) #Param('slug'), but with the slugDto, it can’t work. Instead, it must be just #Param().
Finally, my method :
#Post()
#ApiParam({ name: 'slug', type: String })
public async create(#Param() { slug }: SlugDto, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
And the dto :
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
#IsSlugMatchingAnyExistingPoll()
slug: string;
}
Personally, I wouldn't register this as a class-validator decorator, because these are beyond the scopes of Nestjs's dependency injection. Getting a grasp of a service/database connection in order to check the existence of a poll would be troublesome and messy from a validator constraint. Instead, I would suggest implementing this as a pipe.
If you want to only check if the poll exists, you could do something like:
#Injectable()
export class VerifyPollBySlugPipe implements PipeTransform {
constructor(#InjectRepository(Poll) private repository: Repository<Poll>) {}
transform(slug: string, metadata: ArgumentsMetadata): Promise<string> {
let found: Poll = await repository.findOne({
where: { slug }
});
if (!found) throw new NotFoundException(`No poll with slug ${slug} was found`);
return slug;
}
}
But, since you're already fetching the poll entry from the database, maybe you can give it a use in the service, so the same pipe can work to retrieve the entity and throw if not found. I answered a similar question here, you'd just need to add the throwing of the 404 to match your case.
Hope it helps, good luck!

Addtional parameter with #ngrx/entity

I want to keep employee array and page loading status in store state. So my initial state will look like this
const initialState = {
isLoading: false,
employees: []
};
Now i want to use #ngrx/entity for employee instead of array. The documentation only show the demo for using entity with entire state.
How can i use entity for only one property rather than entire state?
If it's not possible what is the alternative for above scenario?
See the docs for an example:
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
export interface User {
id: string;
name: string;
}
export interface State extends EntityState<User> {
// additional entities state properties
selectedUserId: number;
}
export const adapter: EntityAdapter<User> = createEntityAdapter<User>();

StoreModule.forRoot() - how to return object without additional key

I am wondering how can I return object of the same type as reducer function:
function storeReducer(
state = INITIAL_APPLICATION_STATE,
action: Actions
): ApplicationState {
switch (action.type) {
case LOAD_USER_THREADS_ACTION:
return handleLoadUserThreadsAction(state, action);
default:
return state;
}
}
I expect object of type ApplicationState, but with that approach:
StoreModule.forRoot({storeReducer})
I am getting object with key:
storeReducer:{ // object of type Application State}
I am expecting to get object (without additional storeReducer key):
{//object of type Application State}
Tried also StoreModule.forRoot(storeReducer) but then I am getting empty objects and it is not working.
The forRoot method on StoreModule expects and ActionReducerMap, not the result of your reducer.
I typically set mine up in a seperate file like this:
export interface IAppState {
aPieceOfState: IAPieceOfState;
}
export const reducers: ActionReducerMap<IAppState> = {
aPieceOfState: aPieceOfStateReducer
};
Then import this to app.module.ts and use it like:
StoreModule.forRoot(reducers)
Or you can put an assertion StoreModule.forRoot({storeReducer} as ActionReducerMap<IAppState>)

Ngrx: meta-reducers and capturing effects

I've added into my IStore a transaction concept. It straightforwardly stands for providing a way to store into my IStore which pending operations keep pending. When they are completed, they are removed:
export interface IStore {
user: IUser;
txs: ITxRedux;
}
All my reducers are like:
* reducer name: `'OPERATION'`
* success reducer name: `'OPERATION_SUCCESS'`
* failed reducer name: `'OPERATION_FAILED'`
Some of these reducers (only those need a http request) are captured using #Effects:
#Effect({ dispatch: true })
userLogin$: Observable<Action> = this._actions$
.ofType('USER_LOGIN')
.switchMap((action: Action) =>
{
....
});
Currently, my effects have this pattern:
return make_http_call
.map(_ => ({type: 'OPERATION_SUCCESS'}, payload: {...}))
.catch(_ => ({type: 'OPERATION_FAILED'}, payload: {...}));
So, I'd like to get a way by adding or removing a "transaction" into my IStore.txs each time an effect is called or completed. When I say "add a transaction into my IStore.txs" I mean to call transaction reducers:
public static ADD_TX = `ADD_TX`;
private static addTx(txsRdx: ITxRedux, type, payload: ITx) {
const tx = payload;
return {
ids: [ ...txsRdx.ids, tx.id ],
entities: Object.assign({}, txsRdx.entities, {[tx.id]: tx}),
};
}
public static REMOVE_TX = `REMOVE_TX`;
private static removeTx(txsRdx: ITxRedux, type, payload) {
const tx: ITx = payload;
var entitiesTmp = {...txsRdx.entities};
delete entitiesTmp[tx.id];
return {
ids: txsRdx.ids.filter(id => tx.id != id),
entities: entitiesTmp
};
}
I've listen to talk a bit about meta-reducers, but I don't quite whether they are going to be able to get my goal.
Is there any way to get it using a elegant way?
Late reply, but you might find this post useful. The classic example (taken mostly from that post) is to log each action/state change by means of a logging meta-reducer:
export function logging(reducer) {
return function loggingReducer(state, action) {
console.group(action.type);
// invoke following, "wrapped" reducer in the chain
const nextState = reducer(state, action);
console.log(`%c prev state`, `color: #9E9E9E`, state);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
console.groupEnd();
// return wrapped reducer output
return nextState;
};
}
In main app module, you compose the new logging reducer factory with the usual combineReducers reducer factory:
const appReducer = compose(logging, combineReducers)(reducers);
//...
StoreModule.provideStore(appReducer),
Just watchout for setting up StoreModule and global app reducer, as that syntax has changed in recent ngrx versions since that blog post.
On a side note, if you're looking for some inspiration on implementing a meta-reducer to catch and invoke remote API calls, you might want to have a look at an equivalent, already-made middleware for Redux, as redux-api-middleware. HTH

Resources