I have the following store
export const featureAdapter: EntityAdapter<IProduct> = createEntityAdapter<IProduct>({
selectId: model => model.id,
});
export interface State extends EntityState<IProduct> {
selectedProduct?: IProduct;
isLoading?: boolean;
error?: any;
}
export const initialState: State = featureAdapter.getInitialState({
selectedProduct: IProduct,
isLoading: false,
error: null
});
I would like that my selected product always point on a Entity and get updates with it. I believe since actions are always creating new object, the link is not possible, I decided to change the selectedProduct from a reference to a simple id.
export const initialState: State = featureAdapter.getInitialState({
selectedProduct: string,
isLoading: false,
error: null
});
but how do i retrieve my entity with the same ID, and get updates on my observable if it is changed ?
I tried
export const getSelectedProduct: MemoizedSelector<object, any> = createSelector(
selectAllEntities,
selectedProduct,
(entities, id) => entities[id]
);
export const selectEntityById = createSelector(
selectAllEntities,
(entities, props) => entities[props.id]
);
my questions are
Is this the only way to select an entity by id ?
this.store$.pipe(
select(ProductStoreSelectors.selectEntityById, { id: product.id }),
map(product => console.log(product))
)
and
this.store$.select(ProductStoreSelectors.getSelectedProduct).subscribe((product: string) => {
console.log(product)
}),
this never trigger when I change my selected product Id
EDIT :
the reducer on select do the following
const SET_SELECTED_PRODUCT = (state: State, action: featureAction.SetSelectedProduct) => {
return {
...state,
selectedProduct: action.payload.product.id
};
};
Try something like this in your selector:
export const selectEntityById = (id: number) => createSelector(
selectAllEntities,
(entities) => entities[id]
);
and your select:
select(ProductStoreSelectors.selectEntityById(id))
Related
Here my redux state , the state has dynamic nested object name
const search = {
client :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,},
[dynamicKey] :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,},
[dynamicKey2] :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,}
};
I'm trying to get nested object by dynamic key , here is my selector code :
import { createSelector } from "reselect";
export const searchState = (state) => state.search;
export const selectSearch = (keyRef) =>
createSelector([searchState], (search) => search[keyRef]);
You forgot to ask the question but your code looks fine as it is. In the component you can use useMemo to not needlessly create the selector:
//renamed the selector to create...
export const createSelectSearch = (keyRef) =>
createSelector([searchState], (search) => search[keyRef]);
//compnent example
const MyComponent = ({keyRef}) => {
const selectSearch = React.useMemo(
()=>createSelector(keyRef),//create the selector when keyRef changes
[keyRef]
);
const result = useSelector(selectSearch)
return <jsx />
}
Some more information about this pattern can be found here
I have problem to update state in ngrx reducer for action loadSuccess
State after load action
{
dashboard: {
serverList: null,
isLoading: true,
error: 'zero'
}
}
State after loadSuccess action
{
dashboard: {
isLoading: false,
error: 'zero'
}
}
const t1 = (state, action) => {
return ({...state, serverList: action.payload.servers});
};
const reducer = createReducer(initialState,
on(load, state => ({...state, isLoading: true})),
on(loadSuccess, (state, {servers}) => ({...state, serverList: servers})),
// on(loadSuccess, t1)
);
export function dashboardReducer(state: State | undefined, action: Action) {
return reducer(state, action);
}
when I swap on(loadSuccess.... line with t1 state it works. I actually check what the object in parameter is and fetch value what I want. But why it fails for on(loadSuccess, (state, {servers}) => ({...state, serverList: servers}))?
Actions definition
export const load = createAction('[Dashboard Component] Load');
export const loadSuccess = createAction('[Dashboard Component] LoadSuccess', props<{servers: ServerInfo[]}>());
should you share how you define the loadSuccess action?
looks like you have the payload property and should use it as payload.servers.
2nd Parameter of the given function contains the action, not the payload..
on(loadSuccess, (state, {payload}) => ({...state, serverList: payload.servers})),
I found that problem came from ngrx docs, from the definition of effect.
#Injectable()
export class MovieEffects {
loadMovies$ = createEffect(() => this.actions$.pipe(
ofType('[Movies Page] Load Movies'),
mergeMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError(() => EMPTY)
))
)
);
constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}
Action here is defined as { type: '[Movies API] Movies Loaded Success', payload: movies }
After change to use directly defined above action export const loadSuccess = createAction('[Dashboard Component] LoadSuccess', props<{servers: ServerInfo[]}>()); it works
loadSomething$ = createEffect(() => this.actions$.pipe(
ofType(DashboardActions.load),
mergeMap(() => this.service.getServers().pipe(map(servers => Actions.loadSuccess({serverList: servers})))),
catchError(() => of({type: '[Dashboard Component] ServerInfo Loaded Error'}))
)
);
Hello I use ngrx and spring boot to display all products. But I can't display one selected product.
My getProductAction et effect work well.
This is the result in Redux:
Some help please
This is my state and initialState:
export interface ProduitState extends EntityState<Produit>{
isLoading: boolean;
selectedProduitId: any;
error: any;
produits: any;
searchQuery: string;
}
enter image description here
export const produitAdapter: EntityAdapter<Produit> = createEntityAdapter<Produit>({
selectId: (produit: Produit) => produit.id,
sortComparer: false,
});
export const produitInitialState: ProduitState = produitAdapter.getInitialState({
isLoading: true,
selectedProduitId: null,
error: null,
produits: [],
searchQuery: ''
});
export const selectedProduitId = (state: ProduitState) => state.selectedProduitId;
export const selectIsLoading = (state: ProduitState) => state.isLoading;
export const selectError = (state: ProduitState) => state.error;
export const selectSearchQuery = (state: ProduitState) => state.searchQuery;
This my action:
export const getProduitAction = createAction('[Produit] Get Produit', props<{produitId: string}>());
const produitPayload = props<{produit: Produit}>();
export const getProduitSuccessAction = createAction('[Produit] Get Produit Success', produitPayload);
This is my reducer:
on(ProduitActions.getProduitAction, (state, {produitId}) => ({
...state, selectedProduitId: undefined, isLoading: true, isLoaded: false, error: null
})),
This is my effect:
getProduit$ = createEffect(() =>
this.actions$.pipe(
ofType(getProduitAction),
exhaustMap(action => {
alert("getProduit Effects "+ action.produitId);
return this.produitService.getProduit(action.produitId)
.pipe(
//tap(res => console.log(res + "TAG EEEEE")),
map((produit, id) => getProduitSuccessAction({produit})),
catchError(err => this.handleError(err))
)
})
)
);
I do this code to get de current selected id:
public produitsState = createFeatureSelector<state.ProduitState>('produit');
private selectors = state.produitAdapter.getSelectors(this.produitsState);
private selectCurrentProduitId = createSelector(this.produitsState, state.selectedProduitId);
private isLoading = createSelector(this.produitsState, state.selectIsLoading);
private error = createSelector(this.produitsState, state.selectError);
private searchQuery = createSelector(this.produitsState, state.selectSearchQuery);
I do this code to get de current selected product but it doesn't work:
getCurrentProductSelected() {
//console.log("ProduitStore: getCurrentProductSelected()");
//console.log(this.store.select(this.selectCurrentProduitId));
alert(this.getProducts());
return combineLatest(
this.getProducts(),
this.store.select(this.selectCurrentProduitId),
(products, selectedId) => selectedId.map(id => {
alert(id +" getCurrentProductSelected");
alert(products +" getCurrentProductSelected");
return products[id];
})
);
}
I try to get the product but I have nothing:
let id = this.route.snapshot.paramMap.get('id');
this.store.dispatchGetProduitAction(id);
this.produit$ = this.store.getCurrentProductSelected();
this.produit$.pipe(
map(produit =>{
alert(produit.nom + " this.produit$");
})
);
Help please. Thanks
It does not look like you set 'selectedProduitId' anywhere. And without that, how would 'getCurrentProductSelected' return anything. If i understand your idea correctly, you need to do something like this:
on(ProduitActions.getProduitAction, (state, {produit}) => ({
...state, selectedProduitId: produit.id
})),
But it's just a blind guess.
How do I combine reducers, when one of them needs props?
I have following model:
interface Device {
id: string;
data: IDeviceData;
}
and DeviceReducer that looks as follow:
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
import { Device } from '../model/device';
import { SubnetBrowserApiActions } from 'src/app/explorer/actions';
export interface State extends EntityState<Device> { }
export const adapter: EntityAdapter<Device> = createEntityAdapter<Device>();
export const initialState: State = adapter.getInitialState();
export function reducer(
state = initialState,
action:
| SubnetBrowserApiActions.SubnetBrowserApiActionsUnion
): State {
switch (action.type) {
case SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces: {
return adapter.upsertMany(action.payload.entries, state);
}
default: {
return state;
}
}
}
const {
selectAll,
} = adapter.getSelectors();
export const getAllDevices = selectAll;
In my other reducer, when I want to select devices using an array of ids I use this code:
export const getVisibleDrives = createSelector(
[fromRoot.getAllDevices, getVisibleDrivesSerialNumbers],
(devices, visibleDevicesIds) =>
devices.filter((device) => onlineDevicesIds.includes(device.serialNumber)),
);
This code is very repetitive, so I'd like to add add parametrized selector that will return just these drives, that have id in array that I pass as prop. What I tried to do looks as follows:
Additional selector in DeviceReduced
export const getDrivesWithIds = (ids: string[]) => createSelector(
getAllDevices,
devices => devices.filter(device => ids.includes(device.id))
);
And then combine them in the following way:
export const getVisibleDrives = createSelector(
getVisibleDrivesSerialNumbers,
(ids) => fromRoot.getDrivesWithIds
);
Issue here is that the returned type of this selector is
(ids: string[]) => MemoizedSelector<State, Device[]>
Which makes it impossible for me to do anything useful with this selector. As an example I'd like to filter this list by keyword, and I am not able to use filter method on it:
Example usage
export const getFilteredVisibleDrives = createSelector(
[getVisibleDrives, getKeywordFilterValue],
(visibleDrives, keywordFilter) => {
return visibleDrives
.filter(drive => // in this line there is an error: Property 'filter' does not exist on type '(ids: string[]) => MemoizedSelector<State, Device[]>'
drive.ipAddress.toLowerCase().includes(keywordFilter.toLowerCase()) ||
drive.type.toLowerCase().includes(keywordFilter.toLowerCase()) ||
drive.userText.toLowerCase().includes(keywordFilter.toLowerCase())
);
},
);
See my post NgRx: Parameterized selectors
for more info.
Update: NgRx v13+
Selector with props are deprecated, use selector factories instead:
Selector:
export const getCount = (props: {id: number, multiply:number}) =>
createSelector(
(state) => state.counter[props.id],
(counter) => counter * props.multiply
);
Component:
this.counter2 = this.store.pipe(
select(fromRoot.getCount({ id: 'counter2', multiply: 2 })
);
this.counter4 = this.store.pipe(
select(fromRoot.getCount({ id: 'counter4', multiply: 4 })
);
Deprecated
Selector:
export const getCount = () =>
createSelector(
(state, props) => state.counter[props.id],
(counter, props) => counter * props.multiply
);
Component:
this.counter2 = this.store.pipe(
select(fromRoot.getCount(), { id: 'counter2', multiply: 2 })
);
this.counter4 = this.store.pipe(
select(fromRoot.getCount(), { id: 'counter4', multiply: 4 })
);
I am learning how to unit test a simple action creator as seen below and want to find out the best way to test it. I've been going off an example from the redux docs on writing tests but wonder if it is possible to test async actions with lambda chaining.
Action:
export const toggleSelect = (id, key) => dispatch => {
return dispatch({
type: TOGGLE_LIST_ITEM,
payload: { id, key },
});
};
Test (jest)
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from '../';
import * as types from '../types';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('list actions', () => {
it('should create an action to unselect all list items', () => {
const id = '123';
const key = 'selectedProspects';
const expectedAction = {
type: types.UNSELECT_ALL_OF_TYPE,
key,
};
const store = mockStore();
return actions.toggleSelect(id, key).then(() => {
expect(store.getActions()).toEqual(expectedAction);
});
});
});
does anyone know of a good way to test this? I am not sure if this not working being indicative to writing more testable code, or if I am just missing something.
You are very close, you just need to make use of dispatch:
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('list actions', () => {
it('should create an action to unselect all list items', () => {
const id = '123';
const key = 'selectedProspects';
const expectedAction = {
type: types.UNSELECT_ALL_OF_TYPE,
key,
};
const store = mockStore();
return store.dispatch(actions.toggleSelect(id, key)).then(() => {
expect(store.getActions()).toEqual(expectedAction);
});
});
});
Your action itself doesn't need to be a thunk. You could easily write that as an action creator... i.e.
export function toggleSelect(id, key) {
return {
type: TOGGLE_LIST_ITEM,
payload: { id, key },
};
}