How does one write selectors for ngrx/data? [duplicate] - ngrx

I have a state and I'd like to create selectors from ngrx/data entities.
import {
Action,
ActionReducer,
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '#ngrx/store';
import {environment} from '../../environments/environment';
import * as fromRouter from '#ngrx/router-store';
import * as fromDrawer from './drawer';
import {InjectionToken} from '#angular/core';
import {NavigationItem} from '../models/navigation-item';
export interface State {
router: fromRouter.RouterReducerState<any>;
drawerNavigationItems: fromDrawer.State;
}
export const ROOT_REDUCERS = new InjectionToken<ActionReducerMap<State, Action>>('Root reducers token', {factory: () => ({
router: fromRouter.routerReducer,
drawerNavigationItems: fromDrawer.reducer,
}),
});
export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
export const selectRouter = createFeatureSelector<
State,
fromRouter.RouterReducerState<any>
>('router');
const {
selectQueryParams, // select the current route query params
selectQueryParam, // factory function to select a query param
selectRouteParams, // select the current route params
selectRouteParam, // factory function to select a route param
selectRouteData, // select the current route data
selectUrl, // select the current url
} = fromRouter.getSelectors(selectRouter);
export const selectRouteId = selectRouteParam('id');
export const selectStatus = selectQueryParam('status');
// Drawer
export const selectDrawerNavigationItems = (state: State) => state.drawerNavigationItems.items as NavigationItem[];
How do I use the pre-defined selectors or write my own with entities or services that came from ngrx/data?
As an example I'd like to create a selector that selects all "Community" entities and then, step 2, select 1 by selectRouteId.
If you imagine a route /communities/:id, selectRouteId returns the Id, and now I'd like the data from the CommunityService and use the selector created or somehow imported and used in step 1 and return a result, 1 Community with the selectRouteId's id, so I can later do something like this.store.dispatch(selectCommunityByCurrentRouteId);
This question is specific to #ngrx/data.

a supplementary answer, to concretely answer my own question,
here's what the reducers/index.ts looks like now
import {
Action,
ActionReducer,
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '#ngrx/store';
import {environment} from '../../environments/environment';
import * as fromRouter from '#ngrx/router-store';
import * as fromDrawer from './drawer';
import {InjectionToken} from '#angular/core';
import {NavigationItem} from '../models/navigation-item';
import {EntitySelectorsFactory} from '#ngrx/data';
import {Community} from '../models/community';
export interface State {
router: fromRouter.RouterReducerState<any>;
drawerNavigationItems: fromDrawer.State;
}
export const ROOT_REDUCERS = new InjectionToken<ActionReducerMap<State, Action>>('Root reducers token', {factory: () => ({
router: fromRouter.routerReducer,
drawerNavigationItems: fromDrawer.reducer,
}),
});
export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
export const selectRouter = createFeatureSelector<
State,
fromRouter.RouterReducerState<any>
>('router');
const {
selectQueryParams, // select the current route query params
selectQueryParam, // factory function to select a query param
selectRouteParams, // select the current route params
selectRouteParam, // factory function to select a route param
selectRouteData, // select the current route data
selectUrl, // select the current url
} = fromRouter.getSelectors(selectRouter);
export const selectRouteId = selectRouteParam('id');
// export const selectStatus = selectQueryParam('status');
// Data
export const communitySelectors = new EntitySelectorsFactory().create<Community>('Community');
export const selectCommunityByRouteId = createSelector(
selectRouteId,
communitySelectors.selectEntities,
(id, communities) => communities.find(c => c.id === id)
);
// Drawer
export const selectDrawerNavigationItems = (state: State) => state.drawerNavigationItems.items as NavigationItem[];
You create a selector for the Community model with
export const communitySelectors = new EntitySelectorsFactory().create<Community>('Community');
and then you combine those two and return 1 Community by the route id.
export const selectCommunityByRouteId = createSelector(
selectRouteId,
communitySelectors.selectEntities,
(id, communities) => communities.find(c => c.id === id)
);
really simple, you pick the input streams, provide a projection function and return the result.
Later, in the component
export class OneCommunityComponent implements OnInit {
community$: Observable<Community>;
constructor(
private store: Store<State>,
) {
this.community$ = this.store.select(selectCommunityByRouteId);
}
}

See https://github.com/peterbsmith2/platform/blob/b2f17bfcc987bf63d10dd207263c0ca2a2e44373/projects/ngrx.io/content/guide/data/extension-points.md#custom-selectors.
/* src/app/reducers/index.ts */
import * as fromCat from './cat.reducer';
import { Owner } from '~/app/models'
export const ownerSelectors = new EntitySelectorsFactory().create<Owner>('Owner');
export interface State {
cat: fromCat.State;
}
export const reducers: ActionReducerMap<State> = {
cat: fromCat.reducer
};
export const selectCatState = (state: State) => state.cat;
export const {
selectAll: selectAllCats
} = fromCat.adapter.getSelectors(selectCatState);
export const selectedCatsWithOwners = createSelector(
selectAllCats,
ownerSelectors.selectEntities,
(cats, ownerEntities) => cats.map(c => ({
...c,
owner: ownerEntities[c.owner]
}))
);

The best solution in your situation it's adapters is very nice.
You can look just here : https://ngrx.io/guide/entity/adapter
You can remove, add, update every object in your store with that and very easily.
You just need to extend your state :)

Related

How to connect RTKQ with CreateSlice? No date in store from slice

Main problem: no datę in store from slice.
Description: I want to create shop. I used to RTKQ to fetch date from endpoint and slice to create shop cart (increase, decrease, remove, add etc).
Problem is that I can connect it to one working system. I read a lot that it is not recommended and you try to prevent from that thing. But I also read that this is possible.
I read the solution that I can put as extraReducers to slice, but I want know how to connect it by store.
import { configureStore } from "#reduxjs/toolkit";
import { setupListeners } from "#reduxjs/toolkit/dist/query";
import { DummyShopApi } from "./reducers/itemSlice";
import { cartSlice } from "./reducers/cartSlice";
import {reducerPath} from "./reducers/itemSlice";
import { combineReducers } from 'redux'
export const rootReducer = combineReducers({
card: cartSlice,
[DummyShopApi.reducerPath]: DummyShopApi.reducer
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(DummyShopApi.middleware),
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
setupListeners(store.dispatch);
it's common question and you could find the solution in documentation, but...))) I'll explain
When you have some query, what looks like:
export const pizzaApi = api.injectEndpoints({
endpoints: (builder) => ({
getPizzas: builder.Query<pizzasResponse, any>({
query: ({ sortBy }) => {
const sortBy1 = `sortBy=${sortBy ? sortBy : 'price'}`;
return {
url: `api?${sortBy1}`
};
}
})
})
});
export const { useGetPizzasQuery } = pizzaApi;
You get a 'queryHook' called useGetPizzasQuery. To add fetched pizzas from it to state (pizzaSlice), you should define extra reducer in your's slice:
extraReducers: (builder) => {
builder
.addMatcher(pizzaApi.endpoints.getPizzas.matchFulfilled, (state, action) => {
state.pizzas = action.payload.pizzas.content;
});
}

NGRX Select returning Store not a value

catalogueSelection in Store Image
I have the data I require in state (catalogueSelection: searchTextResult & categoryCheckboxResult) and need to pass the string 'SearchTextResult' into one component and the Array 'categoryCheckboxResult' into another.
When I try to retrieve the required values I am retrieving the whole store. I have looked at numerous websites and entries here but getting very confused now.
Model:
export class SearchTextResult {
searchTextResult: string;
}
export class CategoryCheckboxResult {
categoryCheckboxResult:Array<CategoryCheckboxResult>;
}
Actions:
import { Action } from '#ngrx/store';
import { SearchTextResult, CategoryCheckboxResult } from 'app/#core/services/products/products.model';
export enum UserCatalogueSelectionTypes {
AddSearchTextResult = '[SearchTextResult] AddResult',
AddCategoryCheckboxResult = '[CategoryCheckboxResult] AddResult',
GetSearchTextResult = '[SearchTextResult] GetResult',
}
export class AddSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddSearchTextResult;
constructor(public payload: SearchTextResult){
}
}
export class AddCategoryCheckboxResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddCategoryCheckboxResult;
constructor(public payload: CategoryCheckboxResult){
}
}
export class GetSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.GetSearchTextResult;
}
export type UserCatalogueSelectionUnion =
| AddSearchTextResult
| AddCategoryCheckboxResult
| GetSearchTextResult
Reducers:
import { SearchTextResult, CategoryCheckboxResult} from "app/#core/services/products/products.model";
import { UserCatalogueSelectionTypes, UserCatalogueSelectionUnion} from "../actions/products.actions";
export interface UserCatalogueSelectionState {
searchTextResult: SearchTextResult | null;
categoryCheckboxResult: CategoryCheckboxResult | null;
}
export const initialState: UserCatalogueSelectionState = {
searchTextResult: null,
categoryCheckboxResult: null,
}
export function reducer(state:UserCatalogueSelectionState = initialState, action: UserCatalogueSelectionUnion ): UserCatalogueSelectionState{
switch (action.type) {
case UserCatalogueSelectionTypes.AddSearchTextResult:
return {
...state,
searchTextResult: action.payload,
};
case UserCatalogueSelectionTypes.AddCategoryCheckboxResult:
return {
...state,
categoryCheckboxResult: action.payload,
};
case UserCatalogueSelectionTypes.GetSearchTextResult: {
return state;
}
default: {
return state;
}
}
}
Selectors:
import { createSelector,createFeatureSelector } from "#ngrx/store";
import {UserCatalogueSelectionState} from '../../store/reducer/products.reducer';
export const fetchSearchTextResults = createFeatureSelector<UserCatalogueSelectionState>("searchTextResult");
export const fetchSearchTextResult = createSelector (
fetchSearchTextResults,
(state:UserCatalogueSelectionState) => state.searchTextResult.searchTextResult
);
export const fetchCatalogueCheckBoxResults = createFeatureSelector<UserCatalogueSelectionState>("catalogueCheckboxResult");
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) => state.categoryCheckboxResult.categoryCheckboxResult
);
My Component 1
Observable:
public searchTextResult: Observable<String>;
Contructor: (part of)
private store: Store<fromCatalogueSelection.UserCatalogueSelectionState>
Code Snippet: (asking for the data)
this.searchTextResult = this.store.select('SearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult);
Console:
TESTING SEARCH TEXT: Store {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: >ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
My Component 2
Observable
searchTextResult$: Observable<CatalogueSelectionActions.GetSearchTextResult>;
Code Snippet: (asking for the data)
this.searchTextResult$ = this.store.select('GetSearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult$);
Console:
TESTING SEARCH TEXT: Store {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: > ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
I've given up on the Selectors for the moment. Any help much appreciated as I'm going a round in circles.
You are almost there, the value from the console log is the observable object, everytime you select something from the store, you will get the value wrapped within an observable. You just need to subscribe to it:
this.searchTextResult$ = this.store.select('GetSearchTextResult');
this.searchTextResult$.subscribe((yourData) => console.log(yourData));
Also, since you are working with selectors, use them, you don't have to write the state/selector name:
selector
...
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) =>
state.categoryCheckboxResult.categoryCheckboxResult
);
component
import * as YourSelectors from './store/something/selectors/yourthing.selectors'
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
.subscribe(console.log);
Additionally, try to subscribe using the async pipe delegating that to your template html so you don't have to deal with the subscription in the code, for example:
component
...
export class Component {
searchTextResult$!: Observable<any> // your data type here
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
}
html
<ng-container *ngIf="(searchTextResult$ | async) as result">
<p>Your result value: {{ result }}</p>
</ng-container>

Why Redux action is not Being being dispatched in Redux-Tooklit

I am using react-redux with redux and redux-toolkit. And according to this example, i created an async dispatch that calls the reducer action when resolved.
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null,
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
},
});
export const { getBlogList } = BlogSlice.actions;
export const getBlogListAsync = (user_id) => (dispatch) => {
axios.get(`/api/blog/getblogs/${user_id}`).then((res) => {
console.log(res.data);
dispatch(getBlogList(res.data.result));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
export default BlogSlice.reducer;
I have used it in a component accordingly so that, the component dispatches getBlogListAsync and that logs the res.data but getBlogList is not being dispatched. I tried putting other console.log() but don't understand what is wrong.
A similar Slice is working perfectly with another Component.
It is hard to say for sure what's wrong here because there is nothing that is definitely wrong.
res.data.result?
You are logging res.data and then setting the blog list to res.data.result. My best guess as to your mistake is that res.data.result is not the the correct property for accessing the blogs, but I can't possibly know that without seeing your API.
console.log(res.data);
dispatch(getBlogList(res.data.result));
missing middleware?
Is there any chance that "thunk" middleware is not installed? If you are using Redux Toolkit and omitting the middleware entirely, then the thunk middleware will be installed by default. Also if this were the case you should be getting obvious errors, not just nothing happening.
it seems fine...
I tested out your code with a placeholder API and I was able to get it working properly. Maybe this code helps you identify the problem on your end. Code Sandbox Demo.
import React from "react";
import { createSlice, configureStore } from "#reduxjs/toolkit";
import axios from "axios";
import { Provider, useDispatch, useSelector } from "react-redux";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
}
});
export const { getBlogList } = BlogSlice.actions;
const store = configureStore({
reducer: {
Blog: BlogSlice.reducer
}
});
export const getBlogListAsync = (user_id) => (
dispatch: Dispatch
) => {
// your url `/api/blog/getblogs/${user_id}`
const url = `https://jsonplaceholder.typicode.com/posts?userId=${user_id}`; // placeholder URL
axios.get(url).then((res) => {
console.log(res.data);
// your list: res.data.result <-- double check this
const list = res.data; // placeholder list
dispatch(getBlogList(list));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
const Test = () => {
const dispatch = useDispatch();
const blogs = useSelector(selectBlogList);
const user_id = "1";
return (
<div>
<button onClick={() => dispatch(getBlogListAsync(user_id))}>
Load Blogs
</button>
<h3>Blog Data</h3>
<div>{JSON.stringify(blogs)}</div>
</div>
);
};
export default function App() {
return (
<Provider store={store}>
<Test />
</Provider>
);
}

NGRX; How to apply filter on adapter data

I have this piece of code which I want to apply a filter on. The state uses EntityState and Adapter.
getAllObjects needs to get categories with parentId of 13 and the getAllTextures gets the remaining.
import { Category } from './models/category';
import { createFeatureSelector, createSelector } from '#ngrx/store';
import { AppState } from './app-state';
import { EntityState } from '#ngrx/entity';
import * as fromCategory from './services/reducers/category.reducer';
export interface CategoriesState extends EntityState<Category> {
allCategoriesLoaded: boolean;
}
const getCategories = createFeatureSelector<AppState, CategoriesState>('categories');
export const getAllObjects = createSelector(getCategories, fromCategory.selectAll); // filter(x => x.parentId === 13)
export const getAllTextures = createSelector(getCategories, fromCategory.selectAll); // filter(x => x.parentId !== 13)
export const getAllCategoriesLoaded = createSelector(getCategories, state => state.allCategoriesLoaded);
and this is the reducer:
import {createReducer, on, State} from '#ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
import { Category } from 'src/app/models/category';
import { RefreshCategoryDone, CategoryActionTypes, CategoryActions } from '../actions/category.actions';
import { CategoriesState } from 'src/app/categories-state';
export const categoryAdapter: EntityAdapter<Category> = createEntityAdapter<Category>();
export const initialState: CategoriesState = categoryAdapter.getInitialState({allCategoriesLoaded : false});
export function categoryReducer(state = initialState, action: CategoryActions) {
switch (action.type) {
case CategoryActionTypes.LoadCategories:
categoryAdapter.addMany(action.payload, state);
break;
default:
return state;
}
}
export const {
selectAll,
selectEntities,
selectIds,
selectTotal
} = categoryAdapter.getSelectors();
Is it possible to select data from adapter by condition? If so, how?
Here's a few simple steps to set this up:
1) add a 'selectedId' property to your State and set the initial state to null
export interface State extends EntityState<Category> {
selectedCategoryId: string | null;
}
export const adapter: EntityAdapter<Category> = createEntityAdapter<Category>({
selectId: (category: Category) => category.id,
sortComparer: false,
});
export const initialState: State = adapter.getInitialState({
selectedCategoryId: null,
});
2) Add actions to set the selected ID
// in the reducer, the action does following:
on(ViewReportPageActions.selectCategory, (state, { id }) => ({
...state,
selectedCategoryId: id,
})),
3) add a selector for the selectedId in the reducer
export const getSelectedId = (state: State) => state.selectedReportId;
4) add a selector at your FeatureSelector file, this is where you add the filter
// you've implemented another name for you Feature Selector here
export const getDataState = createFeatureSelector<State, DataState>('data');
export const getCategoryEntitiesState = createSelector(
getDataState,
state => state.categories
);
export const getSelectedCategoryId = createSelector(
getCategoryEntitiesState,
fromCategorys.getSelectedId
);
export const getSelectedCategory = createSelector(
getCategoryEntities,
getSelectedCategoryId,
(entities, selectedId) => {
if (selectedId != "0"){
return selectedId && entities[selectedId];
} else {
//this is what I use, when I want to create a new category.
//I set the selectedId to 0 which indicitates I'm creating a new one
return selectedId && generateMockCategory();
}
}
);
export const getNotSelectedCategory = createSelector(
getCategoryEntities,
getSelectedCategoryId,
(entities, selectedId) => {
// You'll have to test this...
return entities.filter(e => e.id !== selectedID);
}
);
example pages:
Reducer file;
FeatureSelector Page reference function
'selectSelectedBook'
Found the answer:
import { Category } from './models/category';
import { createFeatureSelector, createSelector } from '#ngrx/store';
import { AppState } from './app-state';
import { EntityState } from '#ngrx/entity';
import * as fromCategory from './services/reducers/category.reducer';
import { map } from 'rxjs/operators';
export interface CategoriesState extends EntityState<Category> {
allCategoriesLoaded: boolean;
}
const getCategories = createFeatureSelector<AppState, CategoriesState>('categories');
export const getAllCategories = createSelector(getCategories, fromCategory.selectAll);
export const getAllObjects = createSelector(getAllCategories, (categories) => categories.filter(x => x.categoryId !== 13));
export const getAllTextures = createSelector(getAllCategories, (categories) => categories.filter(x => x.categoryId === 13));
export const getAllCategoriesLoaded = createSelector(getCategories, state => state.allCategoriesLoaded);

How to get some value from Reducer Ngrx

I have the following reducer
import { EntityState, createEntityAdapter } from '#ngrx/entity';
import { createFeatureSelector } from '#ngrx/store';
export const pageAdapter = createEntityAdapter<Page>({
//selectId: (collection: Collection) => collection.id,
});
export interface State extends EntityState<Page> {
}
const defaultPage = {
ids: ['kHnryus'],
entities: {
'kHnryus': {
id: '83nryus',
title: 'How to create a blog with Angular4 Ngrx',
description: 'How to create a blog with Angular4 Ngrx',
}
},
success_create: false
}
export const initialState: State = pageAdapter.getInitialState();
// Reducer
export function pageReducer(
state: State = initialState,
action: actions.PageActions) {
switch (action.type) {
case actions.ADD_ALL: {
return pageAdapter.addAll(action.pages, state);
};
case actions.SUCCESS: {
return {success_create: true}
};
default:
return state;
}
}
// Create the default selectors
export const getPageState = createFeatureSelector<State>('page');
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = pageAdapter.getSelectors(getPageState);
I want to get the boolean variable success_create of the state in my component.
Basically , I want that if there is SUCCESS, I should be able to get a success_create true in the component class. I have no idea as to how to do this and even if it's possible.
If it is, please how can I achieve this?
First make sure to remove defaultPage constant as you are not maintaining it within your reducers and you are already using #ngrx/entity for that. The success_create should be defined as follow:
export interface State extends EntityState<Page> {
success_create: boolean;
}
export const adapter: EntityAdapter<Item> = createEntityAdapter<Page>({
selectId: (page: Page) => page.id,
sortComparer: false
});
export const initialState: State = adapter.getInitialState({
success_create: false,
});
Then, after your default selectors add a third one that uses getPageState selector and gets one step deeper into your state:
export const getSuccessCreate = createSelector(
getPageState,
(state: State) => state.success_create
);
Then make your component/service listening to it directly like:
this.created$ = this.store.pipe(select(fromPages.getSuccessCreate));
note: the pipe is optional here and if used then select should be imported from #ngrx/store as it is done in the official ngrx demo app. pipe allow you to work with lettable rxjs operators. you can read more about it here.

Resources