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'}))
)
);
Related
i want to moving to #reduxjs/toolkit, actually my app use the following packages:
"#reduxjs/toolkit": "^1.8.3",
"react-redux": "^7.2.2",
"#types/react-redux": "^7.1.20",
actually i use this type of action for my redux store :
export function action<T extends string, P>(type: T, payload?: Partial<P>) {
return { type, ...payload };
}
i change my configure store like this :
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), sagaMiddleware],
});
sagaMiddleware.run(rootSaga);
export default store;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
i've created a simple button component like this :
interface FavoriteBtnProps {
className: string;
}
function FavoriteBtn({ className }: FavoriteBtnProps) {
const { windowDisplay } = useAppSelector(getFavoritePlacesState);
const dispatch = useDispatch();
const handleClick = () => {
dispatch(setWindowDisplay(!windowDisplay));
};
return (
<div className={className} onClick={handleClick}>
<FontAwesomeIcon icon={faBookmark} />
</div>
);
}
export default FavoriteBtn;
it work's fine but need to use generic redux dispatch (Dispatch()). the useAppDispatch created don't work with old actions
now i want to add in a parent component of FavoriteBtn
const { leftPanelDisplay } = useAppSelector(getPanelState);
const { windowDisplay } = useAppSelector(getFavoritePlacesState);
console.log(windowDisplay);
const dispatch = useDispatch();
const handleCollapseIcon = () => {
dispatch(
leftPanelDisplay
? panelsActions.leftPanel.collapse()
: panelsActions.leftPanel.uncollapse()
);
// dispatch(setWindowDisplay(!windowDisplay));
};
this code works fine the console log give me the good value, but if i uncomment
dispatch(setWindowDisplay(!windowDisplay));
i have this error :
Uncaught TypeError: Cannot destructure property 'windowDisplay' of
'Object(...)(...)' as it is undefined.
and i don't see favoriteAndHistoric state in the reduxDevTools
only the state created by the toolkit crashes.
here my slice
const favoritePlacesSlice = createSlice({
name: 'favoritePlaces',
initialState,
reducers: {
setWindowDisplay: (state, action: PayloadAction<boolean>) => {
state.windowDisplay = action.payload;
},
setFavorites: (state, action: PayloadAction<FavoriteSearch[]>) => {
state.favoritePlaces = action.payload;
},
},
extraReducers: (builder) => {
// GET ALL
builder.addCase(fetchFavoriteSearchs.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(fetchFavoriteSearchs.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = action.payload ?? [];
});
// ADD
builder.addCase(addFavoriteSearch.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(addFavoriteSearch.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = state.favoritePlaces.concat(action.payload);
});
// DELETE
builder.addCase(deleteFavoriteSearch.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(deleteFavoriteSearch.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = state.favoritePlaces.filter(
(f) => f.idIri !== action.payload
);
});
},
});
export const { setFavorites, setWindowDisplay } = favoritePlacesSlice.actions;
export default favoritePlacesSlice.reducer;
my rootReducer :
import { combineReducers } from 'redux';
...
import favoritePlacesReducer from '../features/favoritePlaces/favoritePlacesSlice';
const rootReducer = combineReducers({
...
favoritePlacesReducer,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
i've allready tryed to reinstall #reduxjs/toolkit and react-redux from scratch but don't fix the problem and more, i have type errors on createAsyncThunk.
why it works on a componant and not on an other?
thx for help
I'm having trouble refactoring with createSlice, I'm a beginner with redux-toolkit and have looked through the documentation but still having problems.if someone could point me in the right direction that would be fantastic. This is the working code
const SET_ALERT = 'setAlert';
const REMOVE_ALERT = 'alertRemoved';
export const setAlert =
(msg, alertType, timeout = 5000) =>
(dispatch) => {
const id = nanoid();
dispatch({
type: SET_ALERT,
payload: { msg, alertType, id },
});
setTimeout(() => dispatch({ type: REMOVE_ALERT, payload: id }), timeout);
};
const initialState = [];
export default function alertReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_ALERT:
return [...state, payload];
case REMOVE_ALERT:
return state.filter((alert) => alert.id !== payload);
default:
return state;
}
}
Your current setAlert action creator creates a thunk action (an action which takes dispatch as an argument) so it cannot be an action creator that is automatically generated by createSlice.
createSlice
You can keep the setup very similar to what you have now. You would have two separate actions for setting and removing an alert and a thunk for dispatching both. The underlying basic actions can be created with createSlice.
import { createSlice, nanoid } from "#reduxjs/toolkit";
const slice = createSlice({
name: "alerts",
initialState: [],
reducers: {
addAlert: (state, action) => {
// modify the draft state and return nothing
state.push(action.payload);
},
removeAlert: (state, action) => {
// replace the entire slice state
return state.filter((alert) => alert.id !== action.payload);
}
}
});
const { addAlert, removeAlert } = slice.actions;
export default slice.reducer;
export const setAlert = (msg, alertType, timeout = 5000) =>
(dispatch) => {
const id = nanoid();
dispatch(addAlert({ msg, alertType, id }));
setTimeout(() => dispatch(removeAlert(id)), timeout);
};
CodeSandbox
createAsyncThunk
This next section is totally unnecessary and overly "tricky".
We can make use of createAsyncThunk if we consider opening the alert as the 'pending' action and dismissing the alert as the 'fulfilled' action. It only gets a single argument, so you would need to pass the msg, alertType, and timeout as properties of an object. You can use the unique id of the thunk which is action.meta.requestId rather than creating your own id. You can also access the arguments of the action via action.meta.arg.
You can still use createSlice if you want, though there's no advantage over createReducer unless you have other actions. You would respond to both of the thunk actions using the extraReducers property rather than reducers.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
export const handleAlert = createAsyncThunk( "alert/set", (arg) => {
const { timeout = 5000 } = arg;
return new Promise((resolve) => {
setTimeout(() => resolve(), timeout);
});
});
export default createReducer(initialState, (builder) =>
builder
.addCase(handleAlert.pending, (state, action) => {
const { alertType, msg } = action.meta.arg;
const id = action.meta.requestId;
// modify the draft state and don't return anything
state.push({ alertType, msg, id });
})
.addCase(handleAlert.fulfilled, (state, action) => {
const id = action.meta.requestId;
// we are replacing the entire state, so we return the new value
return state.filter((alert) => alert.id !== id);
})
);
example component
import { handleAlert } from "../store/slice";
import { useSelector, useDispatch } from "../store";
export const App = () => {
const alerts = useSelector((state) => state.alerts);
const dispatch = useDispatch();
return (
<div>
{alerts.map((alert) => (
<div key={alert.id}>
<strong>{alert.alertType}</strong>
<span>{alert.msg}</span>
</div>
))}
<div>
<button
onClick={() =>
dispatch(
handleAlert({
alertType: "success",
msg: "action was completed successfully",
timeout: 2000
})
)
}
>
Success
</button>
<button
onClick={() =>
dispatch(
handleAlert({
alertType: "warning",
msg: "action not permitted"
})
)
}
>
Warning
</button>
</div>
</div>
);
};
export default App;
CodeSandbox
what do i do wrong?
fetchEmail$ = createEffect(() => this.actions$.pipe(
ofType(RDX_EMAIL_CONFIRM_FETCH),
switchMap(ac => axios.post(axiosInstance.post('/api/email-confirm/check-email', {
email: ac.payload.email
}).then(res => {
return {
type: RDX_EMAIL_CONFIRM_FETCH_SUCCESS,
};
}).catch(err => {
return {
type: RDX_EMAIL_CONFIRM_FETCH_ERROR
};
})))
))
would noah like to call some api and return an action based on that?
unfortunatley has the following error
Type 'Observable<unknown>' is not assignable to type 'EffectResult<Action>'.
Type 'Observable<unknown>' is not assignable to type 'Observable<Action>'.
Type 'unknown' is not assignable to type 'Action'
here's my reducer maby the problem occurs here
Is it the actions payload definition?
import { createAction, createReducer, on, props } from '#ngrx/store';
import { tassign } from 'tassign';
export const RDX_EMAIL_CONFIRM_FETCH = 'RDX_EMAIL_CONFIRM_FETCH';
export const RDX_EMAIL_CONFIRM_FETCH_SUCCESS = 'RDX_EMAIL_CONFIRM_FETCH_SUCCESS';
export const RDX_EMAIL_CONFIRM_FETCH_ERROR = 'RDX_EMAIL_CONFIRM_FETCH_ERROR';
export const rdxEmailConfirmFetch = createAction(
RDX_EMAIL_CONFIRM_FETCH,
props<{email: string}>()
);
export const rdxEmailConfirmFetchSuccess = createAction(RDX_EMAIL_CONFIRM_FETCH_SUCCESS);
export const rdxEmailConfirmFetchError = createAction(RDX_EMAIL_CONFIRM_FETCH_ERROR);
const initialState = {
isFetch: false
}
export const emailConfirmReducer = createReducer(
initialState,
on(rdxEmailConfirmFetch, (state) => tassign(state, {
isFetch: true
})),
on(rdxEmailConfirmFetchSuccess, (state) => tassign(state, {
isFetch: false
})),
on(rdxEmailConfirmFetchError, (state) => tassign(state, {
isFetch: false
}))
)
You have to use RxJS of operator to convert it to observable, just like below.
fetchEmail$ = createEffect(() => this.actions$.pipe(
ofType(RDX_EMAIL_CONFIRM_FETCH),
switchMap(ac => axios.post(axiosInstance.post('/api/email-confirm/check-email', {
email: ac.payload.email
}).then(res => {
return of({
type: RDX_EMAIL_CONFIRM_FETCH_SUCCESS,
});
}).catch(err => {
return of({
type: RDX_EMAIL_CONFIRM_FETCH_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.
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))