Get list as array from Firebase with angularfire2(v5) and ngrx effects - firebase

I have to tell you I'm getting crazy with it. I'm trying to get data from Firebase with AngularFire2(v.5) then work with it on #ngrx/effects and store it on #ngrx/store. Well, as I need the data with the keys, my code of effects looks like this:
spaces.effects.ts
#Effect()
getSpaces$ = this.actions$.ofType(SpacesActions.GET_SPACES_REQUEST)
.switchMap((action: SpacesActions.GetSpacesRequest) => {
return this.afs.list<Space>('/spaces').snapshotChanges()
.switchMap(actions => {
console.log('action is ', actions);
return actions.map(space => {
const $key = space.payload.key;
const data: Space = { $key, ...space.payload.val() };
console.log('snapshot is: ', data);
return new SpacesActions.GetSpacesSuccess(data);
});
}
);
My "actions" comes with the data and the key, then I get the key for each item because then I could update and delete items easily. My database has 3 items with 3 keys. If I run this code and log it, first I can see all items in 1 array with their payloads and with the second log I see each payload as snapshot.
When I call GetSpacesSuccess, I'd like to send all snapshots I got (with key and item) then store it. The way I'm doing now dispatch this action 3 times and I can see only 2 items on the screen because the first one is overridden by the second one.
So, two questions: Is there any easier way to get the items from firebase with their keys then store them with #ngrx? If not, what am I doing wrong that my first item is being overridden and my action is being dispatched 3 times?
Please, I'm doing my best with it as I'm learning. Thank you!
spaces.reducers.ts
case SpacesActions.GET_SPACES_REQUEST:
return {
state,
spaces: null,
loading: true
};
case SpacesActions.GET_SPACES_SUCCESS:
return {
...state,
...action.payload,
spaces: [state, action.payload],
loading: false
};
spaces.actions.ts
export class GetSpacesRequest implements Action {
readonly type = GET_SPACES_REQUEST;
}
export class GetSpacesSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space) {} <<<<<HERE I'D LIKE TO GET THE FULL ARRAY WITH EACH KEY
}

Thanks #AndreaM16 for the most complete answer. I went through the night working on it and I ended up doing it different. Actually, in the learning process we make mistakes in order to get the knowledge. Probably your solution is better than mine and I'll study that, thanks. Please, if possible, I'd love to hear your comments about my solution.
Finally, after reading lots of documentation, my effects is now this one, I don't have any error catcher though:
private spacesList = 'spaces/';
#Effect()
getSpaces$ = this.actions$.ofType(SpacesActions.GET_SPACES_REQUEST)
.switchMap(payload => this.afs.list(this.spacesList).snapshotChanges()
.map(spaces => {
return spaces.map(
res => {
const $key = res.payload.key;
const space: Space = {$key, ...res.payload.val()};
return space;
}
);
})
.map(res =>
new SpacesActions.GetSpacesSuccess(res)
));
Reducer
case SpacesActions.GET_SPACES_REQUEST:
return Object.assign({}, state, {
spaces: null,
loading: true
});
case SpacesActions.GET_SPACES_SUCCESS:
return Object.assign({}, state, {
spaces: action.payload,
loading: false
});
Actions
export class GetSpacesRequest implements Action {
readonly type = GET_SPACES_REQUEST;
}
export class GetSpacesSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space[]) {}
}
And, in my component, where I need the list:
constructor(private store: Store<fromSpaces.FeatureState>) {}
ngOnInit() {
this.store.dispatch(new SpacesActions.GetSpacesRequest());
this.spacesState = this.store.select('spaces');
}

If I understood your question correctly, you would like to store for each Item also store its key. You are looking for Map.
I would structure your feature as follows.
spaces.actions.ts:
Loading spaces requires no payload, while success has only an array of Space. I think you should build your Map<string,Space> in your reducer (string is your key).
import { Action } from '#ngrx/store';
/** App Models **/
import { Space } from './space.model';
export const GET_SPACES = '[Spaces] Spaces get';
export const GET_SPACES_SUCCESS = '[Start] Spaces get - Success';
export class GetSpacesAction implements Action {
readonly type = GET_SPACES;
}
export class GetSpacesActionSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space[]) {}
}
export type All
= GetSpacesAction
| GetSpacesActionSuccess;
spaces.effects.ts:
I'm assuming you just need a method to fetch spaces. If you need to do other stuff, just edit this piece of code. spaceService.getSpaces() is supposed to return only an array of Spaces. So, create a new Space model and, on your service, map each json entry to a new Space().
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
/** rxjs **/
import {map} from 'rxjs/operators/map';
import {mergeMap} from 'rxjs/operators/mergeMap';
import {catchError} from 'rxjs/operators/catchError';
/** ngrx **/
import * as spacesActions from './spaces.actions';
/** App Services **/
import { SpacesService } from './spaces.service';
#Injectable()
export class SpacesEffects {
#Effect() getSpaces$ = this.actions$
.ofType(spaceActions.GET_SPACES)
.pipe(
mergeMap(() => {
return this.spaceService.getSpaces()
.pipe(
map((spaces) => {
return new spacesActions.GetSpacesActionSuccess(spaces);
}),
catchError((error: Error) => {
// Handle erro here
})
);
})
)
;
constructor(private spacesService: SpacesService, private actions$: Actions) { }
}
spaces.reducer.ts
Here you build your map and you can also create a new action to return, for instance, a space given its key. I dont think you need any loading parameter here, I guess you are using it for some loading handling in your views, just use AsyncPipe in your view and handle a loading animation with an *ngIf checking if there are spaces or not.
/** ngrx **/
import {createFeatureSelector} from '#ngrx/store';
import {createSelector} from '#ngrx/store';
import * as spacesActions from './spaces.actions';
export type Action = spacesActions.All;
/** App Models **/
import { Space } from './space.model';
export interface SpaceState {
keySpaces: Map<string, Space>;
spaces: Space[];
keys: string[];
}
export const initialState: SpaceState = {
keySpaces: new Map<string, Space>(),
spaces: [],
keys: []
};
// Selectors
export const selectSpace = createFeatureSelector<SpaceState>('space');
export const getKeySpaces = createSelector(selectSpace, (state: StartState) => {
return state.keySpaces;
});
export const getSpaces = createSelector(selectSpace, (state: SpaceState) => {
return state.spaces;
});
export const getKeys = createSelector(selectSpace, (state: SpaceState) => {
return state.keys;
});
export function spacesReducer(state: SpaceState = initialState, action: Action): SpaceState {
switch (action.type) {
case startActions.GET_SPACES_SUCCESS:
// Since we return this from effect
const fetchedSpaces = action.payload;
const fetchedKeys = [];
const keySpacesMap = new Map<string, Space>();
fetchedSpaces.forEach( (space: Space) => {
fetchedkeys = fetchedKeys.concat(space.key);
keySpacesMap.set(space.key, new Space(space));
}
returns {
...state,
keySpaces: keySpacesMap,
spaces: fetchedSpaces,
keys: fetchedkeys
}
default: {
return state;
}
}
}
And, finally, you should be able to get such parameters in your components like:
. . .
keySpaces$ = Observable<Map<string, Space>>;
spaces$ = Observable<Array<Space>>;
keys$ = Observable<Array<string>>;
constructor(private _store: Store<AppState>) {
this.keySpaces$ = this._store.select(getKeySpaces);
this.space$s = this._store.select(getSpaces);
this.keys$ = this._store.select(getKeys);
}
. . .
ngOnInit() {
this._store.dispatch(new spacesActions.GetSpacesAction);
}
. . .
Of course add the new state to AppState:
. . .
export interface AppState {
. . .
space: SpaceState;
}

Related

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>

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.

#ngrx 4 how to filter current loaded data

I am working on a new angular 4 plus #ngrx 4 project.
I wish to have a searching function on the loaded data.
For example, all the contacts info have been loaded in the component.
The contacts list will be filtered which contact name matched with the search text.
Please see screenshot
As the data is existed in store and I do not wish to call web api service again.
Any idea or demo code would be appreciated.
You can follow this flow to search what you need on already fetched content:
Use something like '(input)'='searchInputChange$.next(search)' in your input. So, each time the user changes the input, it will trigger our research.
Then, on your component, on the constructor, each time searchInputChange$ changes, we trigger a new SearchAction. Then, we will change our filtered contents on the reducers and the result will be inserted into contents$. On ngOnInit we just load the data from api the first time.
I'm using a model called Content, just an example, that has a string parameter title. We will use this field to filter our contents based on the search input.
import { Component, OnInit } from '#angular/core';
import { Store } from '#ngrx/store';
import { Subject } from 'rxjs/Subject';
import {of} from 'rxjs/observable/of';
/** ngrx **/
import {AppState} from '../../app-state.interface';
import * as searchActions from './actions/search.actions';
/** App Models **/
import { Content } from './models/content.model';
export class SearchComponent implements OnInit {
searchInputChange$ = new Subject<string>();
contents$: Observable<Array<Content>>;
constructor(private _store: Store<AppState>) {
this.searchInputChange$
.switchMap((text: string) => of(text))
.subscribe((text: string) => this._store.dispatch(new searchActions.SearchAction(text)));
this.contents$ = this._store.select(getSearchedContents);
}
ngOnInit() {
this._store.dispatch(new searchActions.LoadAction());
}
}
Then, we'll have our SearchActions. Load is triggered on the init of our component, fetches some contents from api. LoadSuccess is emitted on the effect of the load action in order to populate our reducer with fetched data and show it in our first component, this has a payload of an array of contents. Search will be triggered on change of our input field, this will have a string payload containing the search string.
import { Action } from '#ngrx/store';
/** App Models **/
import { Content } from '../models/content.model';
export const LOAD = '[Search] Load';
export const LOAD_SUCCESS = '[Search] Load Success';
export const SEARCH = '[Search] Search';
export class LoadAction implements Action {
readonly type = LOAD;
constructor() { }
}
export class LoadActionSuccess implements Action {
readonly type = LOAD_SUCCESS;
constructor(public payload: Content[]) { }
}
export class SearchAction implements Action {
readonly type = SEARCH;
constructor(public payload: string) {}
}
export type All
= LoadAction
| LoadActionSuccess
| SearchAction;
SearchEffect that will just fetch contents from api:
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
/** rxjs **/
import {of} from 'rxjs/observable/of';
import {map} from 'rxjs/operators/map';
import {mergeMap} from 'rxjs/operators/mergeMap';
import {catchError} from 'rxjs/operators/catchError';
/** ngrx **/
import * as searchActions from '../actions/search.actions';
/** App Services **/
import { SomeService } from '../services/some.service';
/** App Model **/
import {Content} from '../models/content.model';
#Injectable()
export class SearchEffects {
#Effect() load$ = this.actions$
.ofType(searchActions.LOAD)
.pipe(
mergeMap(() => {
return this.someService.getContentsFromApi()
.pipe(
map((contents: Content[]) => {
return new searchActions.LoadActionSuccess(contents);
}),
catchError(() => {
// do something
})
);
})
)
;
constructor(private someService: SomeService, private actions$: Actions) { }
}
SearchReducer will handle LoadSuccess when we successfully fetch contents from api and Search action that will filter our fetched contents to return only the ones containing our search string inside content's title parameter. We save first fetched contents in both of contents and searchedContents. Then, on search, we will update searchedContents to contain only contents having content.title including the searched string.
import { isEmpty } from 'lodash';
/** ngrx **/
import {createFeatureSelector} from '#ngrx/store';
import {createSelector} from '#ngrx/store';
/** App Models **/
import { Content } from '../models/content.model';
/** ngrx **/
import * as searchActions from '../actions/search.actions';
export type Action = searchActions.All;
export interface SearchsState {
contents: Content[];
searchedContents: Content[];
}
export const initialState: SearchsState = {
contents: [],
searchedContents: []
};
/ -------------------------------------------------------------------
// Selectors
// -------------------------------------------------------------------
export const selectContents = createFeatureSelector<SearchsState>('search');
export const getSearchedContents = createSelector(selectContents, (state: searchedContents) => {
return state.searchedContents;
});
export function contentsReducer(state: searchedContents = initialState, action: Action): searchedContents {
switch (action.type) {
case contentsActions.LOAD_SUCCESS:
const loadContents = action.payload.map(content => new Content(content));
return {
contents: loadContents,
searchedContents: loadContents
};
case contentsActions.SEARCH:
const keywordContents = isEmpty(action.payload) ? state.contents :
state.contents.filter(content => content.title.includes(action.payload));
return {
contents : state.contents,
searchedContents : keywordContents
};
default: {
return state;
}
}
}
So, updating searchedContents will automatically update the contents in our component.
ngrx store is the part of how you store the data. ngrx store is observable so your application flow is
Container -> Components
Container - wrapper component that will select data from store.
example:
const contacts$: Observable<contact> = this.store.pluck('contacts');
//*contacts$ - the dollar since is convention for Observable *//
Component - data visualization component, the data will be as Input(). example:
Input() contacts: Array<contact>;
this convention is called sometime SmartComponent(Container) and
DumbComponent(component)
now for a data transform/mapping you can use reactive approach(Rxjs) or functional programming or whatever you want but it not related for ngrx because in your contacts component the data as exist.
DEMO FOR YOUR SCENARIO:
contacts.container.ts
#Component({
selector: 'contacts-container',
template: `
<contacts-list [contacts]="contacts$ | async"></contacts-list>
`
})
export class ContactsContainer {
contacts$: Observable<[]contact> = this.store.pluck('contacts');
constructor(
private store: Store<applicationState>
) { }
}
contact-list.component.ts
#Component({
selector: 'contacts-list',
template: `
<input type="text" placeholder="write query" #query>
<ul>
<li *ngFor="contact of contacts | searchPipe: query.target.value">
</li>
</ul
`
})
export class ContactsListComponent {
contcats: Array<contact> = [];
constructor() { }
}
i use searchPipe for data transform ( custom pipe ) but is only example for data transform you can do it else.
Good Luck!

Ngrx: combine two selectors

I've built this IStore:
export interface IStore {
user: IUser;
sources: ISourceRedux;
}
where IUser is:
export interface IUser {
id: string;
cname: string;
sname: string;
...
}
and ISourceRedux is:
export interface ISourceRedux {
entities: { [key: string]: ISource };
ids: Array<string>;
selectedIds: Array<string>;
editingSource: ISource;
defaultId: string;
}
So, I've created these selectors:
export const getSourcesState = (state: IStore) => state.sources;
export const getSelectedIds = (sourceRdx: ISourceRedux) => sourceRdx.selectedIds;
export const getSelectedSourceIds = createSelector(getSourcesState, fromSources.getSelectedIds);
So, up to now, in order to check if a user is logged I did that:
this.store$
.select(fromRoot.getUserState)
.filter(user => user.id != null && user.logged)
.do(user => this.store$.dispatch(...))
...
Now I'm strugling for getting user information and selectedSourceIds at the same time in order to check if:
a user is logged (this.store$.select(fromRoot.getUserState)
then get all selectedSourceIds (this.store.select(fromRoot.getSelectedSourceIds))
dispatch an action
How could I get this?
Would it make sense to add that code to a selector:
// Selector functions
const getProductFeatureState = createFeatureSelector<ProductState>('products');
const getUserFeatureState = createFeatureSelector<UserState>('users');
export const getCurrentProduct = createSelector(
getProductFeatureState,
getUserFeatureState,
getCurrentProductId,
(state, user, currentProductId) => {
if (currentProductId === 0) {
return {
id: 0,
productName: '',
productCode: 'New',
description: 'New product from user ' + user.currentUser,
starRating: 0
};
} else {
return currentProductId ? state.products.find(p => p.id === currentProductId) : null;
}
}
);
This code is in the product.reducer file. Here I define the feature selector both for the products and for the users.
I then build a getCurrentProduct selector using both the product and user feature.
This is my solution:
this.store$.combineLatest(
this.store$.select(fromRoot.getUserEntity),
this.store$.select(fromRoot.getSelectedSourceIds),
(store, user, selectedSourceIds) => ({user: user, selectedSourceIds: selectedSourceIds})
)
.filter((proj) => proj.user.id != null && proj.user.logged)
.do((proj) => this.store$.dispatch({type: 'DELETE_CARDS', payload: {username: proj.user.username, tokens: proj.selectedSourceIds}}))
.take(1)
.subscribe();
I hope it's useful.
I created a feature selector that combines two features to accomplish this.
The feature selector for the global module:
export interface ScaffoldPartialState extends GlobalPartialState {
readonly [SCAFFOLD_FEATURE_KEY]: State;
}
which I import to the Scaffold selectors and have ScaffoldPartialState extend it.
export interface ScaffoldPartialState extends GlobalPartialState {
readonly [SCAFFOLD_FEATURE_KEY]: State;
}
The createFeatureSelector only returns the state typed so that the returned type looks like it contains only the state for this feature.
The value is the complete application state, but the type makes it looks like it's only for one module.
By one type extending the other, the resulting type provides a property for both modules.
AppState
{
module1: { ... },
module2: { ... },
module3: { ... }
}
Module2PartialState
{
module2: { ... },
}
Module2PartialState extends Module3PartialState
{
module2: { ... },
module3: { ... }
}
This way the ScaffoldPartialState feature selector works for selectors of both modules.
Example:
export const getAuthorizedMenuItems = createSelector(
getScaffoldState,
GlobalSelectors.getUserPermissions,
getMenuItems,
(_globalState, userPermissions, menuItems) =>
userPermissions ? menuItems.filter(e => userPermissions.checkAllPermissions(e.permissions)) : []
);

Redux action reuse

I'm a beginner in react / redux.
I've finished a basic component <HeatMap /> in my app, with its actions / reducer / store and it works well.
And I'll render another <HeatMap /> with different settings (props).
What I'm trying to do is to separate this 2 component, because when i dispatch an update action in one, the other one performed it simultaneously.
Question 1
I tried this to separate the states in store
import heatMap from './heat-map1'
import {combineReducers} from 'redux';
export let reducers = combineReducers({
heatMap1: heatMap,
heatMap2: heatMap
});
combineReducers and connectthe 2 heatmap in different object in store
export default connect((state)=> {
return {
onState: state.heatMap1.onState,
config: state.heatMap1.config
}
})(CHMSHeatMap1)
and
export default connect((state)=> {
return {
onState: state.heatMap2.onState,
config: state.heatMap2.config
}
})(CHMSHeatMap2)
is this correct?
Question 2
Because 2 component both react when action is dispatched
I'm thinking about separating the shared actions, but I don't think it's a good idea. Or maybe the issue is not here.
So can you tell me what cause this problem and how to solve it?
Here are my reducer
import * as actionTypes from '../actions/heat-map';
import Immutable from 'immutable';
const onState = {
fetching: 'FETCHING',
error: 'ERROR',
drawn: 'DRAWN'
};
const initialState = {
onState: onState.fetching,
config: {}
};
export default function heatMapReducer(state = initialState, action) {
let immutableState = Immutable.fromJS(state);
switch (action.type) {
case actionTypes.INITIALIZING:
return immutableState.set('onState', onState.drawn).set('config', action.payload.initConfig).toJS();
case actionTypes.FETCH_DATA_REQUEST:
return immutableState.set('onState', onState.fetching).toJS();
case actionTypes.FETCH_DATA_SUCCESS:
return immutableState.set('onState', onState.drawn).setIn(['config','series',0,'data'],Immutable.fromJS(action.payload.mapData.data)).toJS();
case actionTypes.FETCH_DATA_FAILURE:
return immutableState.set('onState', onState.error).set('config', action.payload.mapData).toJS();
default:
return state;
}
}
Action is simple
export function initializeConfig(initConfig) {
return {
type: INITIALIZING,
payload: {
text: 'Initializing',
initConfig
}
}
}
export function requireMapData() {
return {
type: FETCH_DATA_REQUEST,
payload: {
text: 'Loading'
}
};
}
..........
//Async Action for fetching map data and redraw the map
export function fetchMapData(address) {
return function (dispatch) {
//dispatch requireMapData action to set the map in loading state
dispatch(requireMapData());
return fetch(address)
.then(fetchUtil.checkHttpStatus) //check if 404
.then(fetchUtil.parseJSON)
.then(mapData => dispatch(fetchDataSucceed(mapData)))
.catch(error => dispatch(fetchDataFailed(error)));
}
}
Thank you my friend.
You cannot duplicate your reducers in the manner you've depicted. Both are going to respond in the exact same way to the exact same actions.
The solution is to have all of your heat map data in the same reducer state. e.g.
const initialState = {
heatMap1: {},
heatMap2: {}
};
export default heatmap(state = initialState, action) {
// etc
Now if you want to use the same actions for both heat maps, you'll need to have an action property specifying which heap map you're targeting. If you have several heat maps, I'd recommend an array of heat maps with each action containing an index or id to target a particular heat map. e.g.
function updateHeatMap(index, value) {
return {
type: UPDATE_HEATMAP,
index: index,
value: value
}
}
You can also take a look at the multireducer module (https://github.com/erikras/multireducer). It was designed to solve exactly the scenario you propose.
So you would be able to configure your store as such:
import multireducer from 'multireducer';
import heatMap from './heat-map1'
import {combineReducers} from 'redux';
export let reducers = combineReducers({
multireducer: multireducer({
heatMap1: heatMap,
heatMap2: heatMap
})
});
After that, you would then need to use connectMultireducer() instead of redux's standard connect() in order to connect the specific slice of the store to particular components like so:
export default connectMultireducer((state)=> {
return {
onState: state.heatMap.onState,
config: state.heatMap.config
}
})(CHMSHeatMap)
And finally in order to get the correct part of the state to each of those components you would pass in the key when rendering them as such:
<CHMSHeatMap multireducerKey="heatMap1"/>
<CHMSHeatMap multireducerKey="heatMap2"/>
Obviously it's better to read the actual docs at the multireducer repo, but that should give a brief overview. Basically the module is just abstracting the process of adding a key-based lookup to each reducer that is created through the multireducer function.
I suggest original concept of multireducer working without any libraries.
The base idea is unique Symbol action types and self-contained Redux-module like this:
import * as services from './../../../api/services';
const initialState = {
list: [],
};
function getListReducer(state, action) {
return {
...state,
list: action.payload.list,
};
}
function removeItemReducer(state, action) {
const { payload } = action;
const list = state.list.filter((item, i) => i !== payload.index);
return {
...state,
list,
};
}
export default class List {
constructor() {
// action types constants
this.GET_LIST = Symbol('GET_LIST');
this.REMOVE_ITEM = Symbol('REMOVE_ITEM');
}
getList = (serviceName) => {
return async (dispatch) => {
const list = await services[serviceName].get();
dispatch({
type: this.GET_LIST,
payload: {
list,
serviceName,
},
});
};
}
removeItem = (index) => {
return (dispatch) => {
dispatch({
type: this.REMOVE_ITEM,
payload: {
index,
},
});
};
}
reducer = (state = initialState, action) => {
switch (action.type) {
case this.GET_LIST:
return getListReducer(state, action);
case this.REMOVE_ITEM:
return removeItemReducer(state, action);
default:
return state;
}
}
}
More information read there.

Resources