Dispatch multiple actions from in redux-observable - redux

I am trying to dispatch multiple actions to redux. Here is my code
action$.pipe(
ofType(trigger),
mergeMap(({ payload }) =>
from(endpoint(payload)).pipe(
map(response =>
// this works fine
// setData(response.data)
// this doesn't
concat(
of(setData(response.data)),
of({ type: 'hello' })
)
// I also tried
[
of(setData(response.data)),
of({ type: 'hello' })
]
)
)
),
catchError(err => Promise.resolve(creators.setError(err)))
)
Single dispatch works, but if I try multiple items as above I am getting Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.

map just maps one item to another so when you return [action1, action2] you're still returning an array and redux-observable tries to treat it as an action itself. What you want instead is "unwrapping" the array (or Observable created with concat) returned.
So instead of using map you can use mergeMap (or concatMap) and when you return an array it'll iterate it and make separate emissions for each item:
mergeMap(response => [
setData(response.data),
{ type: 'hello' },
]),
If this looks too odd you can wrap the array with from to make it more obvious:
mergeMap(response => from([
setData(response.data),
{ type: 'hello' },
])),
You can even use a single of:
mergeMap(response => of(
setData(response.data),
{ type: 'hello' },
)),

Related

ngrx effects combine action resposne with multiple selectors values

I have an Action defined MyAction.loadDataSuccess.
When ever loadDataSuccess is success ,we need to dispatch other three action.that am trying to implemented here. Other Actions needed loadDataSuccess response along with selectData1 and selectData2
Here is my code.
loadSuccess$ = createEffect(() => this.actions$.pipe(
ofType(MyActions.loadDataSuccess),
concatMap((action: any) => of(action).pipe(
withLatestFrom(this.store$.select(selectData1)),
withLatestFrom(this.store$.select(selectData2))
)),
switchMap(([payload, data1,data2]: any) => [
FileActions.setSelectedName({ fileName: payload.name[0] }),
PFDataActions.loadPFData({ data1: data1, data2:data2 }),
OtherDataActions.loadOtherData(
{ data1: data1, data2:data2, otherName: ''}
),
MainDataActions.loadMainData(
{ data1: data1, data2:data2, otherName: '' }
),
])
));
The above code was working when we have only one withLatestFrom.
Now the above code throwing FileActions.setSelectedName undefined.
How can we combine action response with two selector values
i have combined both selectData1 and selectData2 into a single selector,but that won't worked.
Some one have any solutions, feel free to update
Note: ngrx version-13
withLatestFrom can take multiple source observables: https://rxjs.dev/api/operators/withLatestFrom
ofType(MyActions.loadDataSuccess),
withLatestFrom(this.store$.select(selectData1), this.store$.select(selectData2)),
switchMap(([payload, [data1,data2]]: any) =>

Laravel9 Inertia::render Array is converted to Vue3 props Object

I have created the following controller function in Laravel 9
public function select(Request $request): Response
{
// Get all diagnostic events (for selection dropdown)
$diagnosticEvents = DiagnosticEventResource::collection(DiagnosticEvent::all());
// Get all answers for selected diagnostic event
$answers = Answer::select('diagnostic_event_id', 'question_id', 'responder_uuid', 'score')
->where('diagnostic_event_id', $request->event_id)
->get();
// Create responder_uuid list and answers array for selected diagnostic event
$responder_uuids = [];
$answerArray = [];
$questions = QuestionResource::collection(Question::all());
foreach($answers as $answer) {
if (!in_array($answer->responder_uuid, $responder_uuids)) {
$responder_uuids[] = $answer->responder_uuid;
}
if (!array_key_exists($answer->question_id, $answerArray)) {
$answerArray[$answer->question_id] = (object)array_merge(
['question' => $questions[$answer->question_id - 1]->description],
['responders' => []]
);
}
if ($answerArray[$answer->question_id]) {
$answerArray[$answer->question_id]->responders[] = (object)array_merge(
['uuid' => $answer->responder_uuid],
['score' => $answer->score]
);
}
}
// Get responder data for selected diagnostic event
$responders = ResponderResource::collection(Responder::whereIn('uuid', $responder_uuids)->get());
return Inertia::render('Answers/Select', [
'diagnosticEvents' => $diagnosticEvents,
'diagnostic_event_id' => $request->event_id == null ? null : (int)$request->event_id,
'answers' => $answerArray,
'responders' => $responders,
'isSchoolAdmin' => Auth::user()->isSchoolAdmin()
]);
}
and a vue3 module starting with the following code
<script setup>
import AuthenticatedLayout from "#/Layouts/Authenticated";
import BreezeLabel from "#/Components/Label";
import {Inertia} from "#inertiajs/inertia";
import {Head} from '#inertiajs/inertia-vue3';
import {ref, watch} from 'vue';
import Index from "#/Pages/Answers/Index.vue";
const props = defineProps ({
diagnosticEvents: Array, // All diagnostic events (for selection)
diagnostic_event_id: Number, // Id for current diagnostic event
answers: Array, // All answers for selected diagnostic event
questions: Array,
responders: Array,
isSchoolAdmin: Boolean
})
When I run the code I will get a warning saying
Invalid prop: type check failed for prop "answers". Expected Array, got Object
When I look at $answerArray in the debugger it is an Array
but when I look at props in Chrome DevTools it shows
answers: {1: {,...}, 2: {,...},...}
instead of
answers: [1: {,...}, 2: {,...},...]
prop responders is also an array included in the Inertia:render response but is transferred correctly
responders: [{uuid: ...},...]
Why and what can I do to fix this?
The problem is that $answerArray is an associative array. Internally, Inertia will call PHP's json_encode, which will turn this into an object. You have two options:
1. Change the answers prop type to Object. Use this if the keys are important to you in the Vue side.
2. Create a non associative array from the $answerArray in order to get json_encode to keep it as an array.
return Inertia::render('Answers/Select', [
// ...
'answers' => array_values($answerArray),
// ...
]);

How can I dispatch one action, wait 1 second, then dispatch two more actions in Redux Observable?

How would I do the following in a single epic
Dispatch pauseGame()
Wait 1 second
Dispatch 2 actions
The following dispatches the last two actions but not pauseGame().
const moveEpic: RootEpic = (action$, state$) =>
action$.pipe(
filter(isActionOf(move)),
map(() => pauseGame()),
filter(() => state$.value.ruleRow.lastMoveSuccessful),
delay(1000),
switchMap(() => [
removeBoardObject(
state$.value.ruleRow.totalMoveHistory[state$.value.ruleRow.totalMoveHistory.length - 1]
.dragged,
),
resumeGame(),
]),
);
The reason pauseGame isn't dispatching is because you're not dispatching it. You're calling the action creator and then immediately changing your observable state to lastMoveSuccessful.
Instead, what you want is to split the pipeline and merge them back to one. I know this is confusing, but it's how Redux-Observable works at the moment. If you want a different way where you can dispatch at any point in time, checkout my article: https://dev.to/sawtaytoes/the-best-practice-anti-pattern-jj6
When the move type occurs, switch to a new observable. That observable is a merging of 2 new observables: one which immediately dispatches pauseGame, the other which checks if the last move was successful, and if so, waits a second and dispatches 2 other actions.
const moveEpic: RootEpic = (action$, state$) => (
action$.pipe(
filter(isActionOf(move)),
switchMap(() => (
merge(
of(pauseGame()),
of(state$.value.ruleRow.lastMoveSuccessful).pipe(
filter(Boolean),
delay(1000),
concatMap(() => [
removeBoardObject(
state$.value.ruleRow.totalMoveHistory[
state$.value.ruleRow.totalMoveHistory.length - 1
].dragged
),
resumeGame(),
]),
)
)
)),
);
)
As a side note, I don't know why you've created your own isActionOf function, but normally you should be able to change that line to ofType(move) instead.
Evert Bouw provided a simpler suggestion utilizing startWith and endWith. You'll lose sequential ordering in your pipeline, but you don't have to split it:
const moveEpic: RootEpic = (action$, state$) => (
action$.pipe(
filter(isActionOf(move)),
switchMap(() => (
of(state$.value.ruleRow.lastMoveSuccessful).pipe(
filter(Boolean),
delay(1000),
map(() => (
removeBoardObject(
state$.value.ruleRow.totalMoveHistory[
state$.value.ruleRow.totalMoveHistory.length - 1
].dragged
)),
startWith(pauseGame()),
endWith(resumeGame()),
)
)
)),
);
)
Keep in mind, if you needed to know the value of state$ in endWith, you'd want to use finalize instead. It takes a function instead of a value.

Dispatch action in effect before return statement

I need to dispatch/trigger another ui-related action inside an effect before calling the service to fetch data without injecting the store
I managed to fix it by injecting the store in the constructor and dispatch this extra action in effect right before calling the service for fetching the data(this.store.dispatch(UIActions.startLoading()) but I am not sure if injecting the store in the effect is a good practice
recipes$ = createEffect(() => this.actions$.pipe(
ofType(RecipeActions.FETCH_RECIPES),
switchMap(() => this.recipeService.getRecipes().pipe(
switchMap(recipes => [
RecipeActions.setRecipes({ recipes }),
UIActions.stopLoading()
]),
catchError(() => EMPTY)
))
));
I wonder if there is a way to do this like using
tap(() => of(UIActions.startLoading())) inside the first switchMap
You're correct in the use of tap operator for your desired functionality.
However, you need to do store.dispatch instead of simply returning an of observable.
Also, instead of multiple switchMap, you can use the from observable to return array of actions.
recipes$ = createEffect(() => this.actions$.pipe(
ofType(RecipeActions.FETCH_RECIPES),
tap(()=> this.store.dispatch(UIActions.startLoading())),
switchMap(() => this.recipeService.getRecipes().pipe(
map(recipes => from([
RecipeActions.setRecipes({ recipes }),
UIActions.stopLoading()
]),
catchError(() => EMPTY)
))
));

Is it possible to both dispatch an array of actions and also navigate from an ngrx effect?

I have an issue with one of my application's ngrx effects. I am basically trying to execute multiple actions using concatMap() AND navigate using the router store's go().
Here is the effect:
#Effect()
loadPersonalInfoAndSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.LOAD_PERSONAL_INFO)
.map((action: LoadPersonalInfoAction) => action.payload)
.do(sessionToken => {
localStorage.setItem('authenticated', 'true');
localStorage.setItem('sessionToken', sessionToken);
})
.switchMap(() => this.userAccountService
.retrieveCurrentUserAccount()
.concatMap(currentUserAccount => [
new LoadUserAccountAction(currentUserAccount),
new SigninAction(),
new LoadMessagesAction({})
])
)
.mapTo(go(['/dashboard']));
If I remove the .mapTo(go(['/dashboard'])), then all three actions in the concatMap array are successfully dispatched to their corresponding effects.
I am therefore wondering why my mapTo(go(... is causing the last two actions in the array (i.e. SigninAction & LoadMessagesAction) not to be dispatched to their corresponding effects..
Can someone please help?
edit: Changing mapTo to do as follows:
.do(go(['/dashboard']));
results in the following error:
ERROR in /Users/julien/Documents/projects/bignibou/bignibou-client/src/app/core/store/session/session.effects.ts (55,9): Argument of type 'Action' is not assignable to parameter of type 'PartialObserver<SigninAction>'.
Type 'Action' is not assignable to type 'CompletionObserver<SigninAction>'.
Property 'complete' is missing in type 'Action'.
Using do for the go call will not see the route changed. go is an action creator and the action that it creates needs to be emitted from the effect for #ngrx/router-store to receive the action and effect the route change.
Also, the mapTo operator will ignore what it receives and will emit the value you've specified, so it's not appropriate, either.
Instead, you should include the action created by the go call in your concatMap array:
#Effect()
loadPersonalInfoAndSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.LOAD_PERSONAL_INFO)
.map((action: LoadPersonalInfoAction) => action.payload)
.do(sessionToken => {
localStorage.setItem('authenticated', 'true');
localStorage.setItem('sessionToken', sessionToken);
})
.switchMap(() => this.userAccountService
.retrieveCurrentUserAccount()
.concatMap(currentUserAccount => [
new LoadUserAccountAction(currentUserAccount),
new SigninAction(),
new LoadMessagesAction({}),
go(['/dashboard'])
])
);

Resources