"Error: Actions must be plain objects" on Array of actions returned from epics - redux

Error: Actions must be plain objects. Use custom middleware for async actions.
On all set of actions returned from epics we get this error.
it could be due to how epics are set up in the epics creator:
const epic = (action$, store) =>
action$.ofType(String(key)).mergeMap(action => func(action, store))
either the mappings are not correct here or more probably they are not correct in the epics.
Example of one epic called by func(action, store):
[String(usersActions.updateUser)]: (action, store) => {
return authFetch(`${API_ROOT}/user/${get(action, 'payload.id')}`, {
method: 'put',
headers: {
...
},
body: ...,
})
.then(resJson => {
if (resJson['status'] === 200) {
return [
appActions.pushNotification('success', USER_UPDATE_SUCCESS),
appActions.setNextPath(`/users/${get(action, 'payload.id')}`),
]
} else
return [
appActions.pushNotification(
'error',
USER_UPDATE_FAILED + ': ' + resJson['message']
),
]
})
.catch(() => {
return [appActions.pushNotification('error', USER_UPDATE_FAILED)]
})
},
It works correctly if the square parenthesis are not there, tried with this suggestion too:
[String(usersActions.updateUser)]: (action, store) => {
return Rx.Observable.fromPromise(
fetch(`${API_ROOT}/user/${get(action, 'payload.id')}`, {
method: 'put',
headers: {},
}).then(res => res.json())
)
.mergeMap(resJson => {
if (resJson['status'] === 200) {
return Rx.Observable.concat(
Rx.Observable.of(
appActions.pushNotification('success', USER_UPDATE_SUCCESS)
),
Rx.Observable.of(
appActions.setNextPath(`/users/${get(action, 'payload.id')}`)
)
)
} else return
Rx.Observable.concat(
Rx.Observable.of(
appActions.pushNotification(
'error',
USER_UPDATE_FAILED + ': ' + resJson['message']
)
)
)
})
.catch(() => {
Rx.Observable.concat(
Rx.Observable.of(
appActions.pushNotification('error', USER_UPDATE_FAILED)
)
)
})
},
it removes the error but the actions dispatched are now duplicated.
Already tried will all the options out there so hopefully it's not a repeated question.

You can add a .do(action => console.log(action)) to the end of your epic to see what value(s) you're emitting and why they are not actions. e.g. if it's an array of actions, that's incorrect. Epics should only ever emit plain old javascript actions with a type property.

Related

What is the different for UseGetSomeQuery Arg Value

I'm new for the RTK Query for redux.
What's the different for auto generated hook in below two ways.
The first way look like correct from the docs but it return 304 network status.
Second way, return 200. working perfectly
1.
const ProjectsList = () => {
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery("projectList") // -- return 304 network status
}
worked fine. but cannot retrieve the object from the store. return.
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery() // -- return 200 network status
Third, the memoized return uninitialize. It seem didn't correct.
// ApiSlice status return uninitialize
import { createSelector, createEntityAdapter } from "#reduxjs/toolkit"
import { apiSlice } from "#/app/api/apiSlice"
const projectsAdapter = createEntityAdapter({})
export const projectsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getProjects: builder.query({
query: () => "/api/projects",
validateStatus: (response, result) => {
return response.status === 200 && !result.isError
},
transformResponse: (responseData) => {
const loadedProjects = responseData.map((project) => {
project.id = project._id
return project
})
return projectsAdapter.setAll(initialState, loadedProjects)
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "Project", id: "LIST" },
...result.ids.map((id) => ({ type: "Project", id })),
]
} else return [{ type: "Project", id: "LIST" }]
},
}),
}),
})
export const {
useGetProjectsQuery,
} = projectsApiSlice
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
// creates memoized selector
const selectProjectsData = createSelector(
selectProjectsResult,
(projectsResult) => {
console.log("projectsResult: ", projectsResult) // -> { isUninitialized: true, status: "uninitialize" }
return projectsResult.data
}
)
export const {
selectAll: selectAllProjects,
selectById: selectProjectById,
selectIds: selectProjectIds,
} = projectsAdapter.getSelectors(
(state) => selectProjectsData(state) ?? initialState
)
Since your query function is just query: () => "/api/projects" (so, not using the argument in any way), both will make exactly the same request for the same resource.
There is no difference between them and every difference you see is probably something random happening on the server and not bound to either invocation.
As for retrieving from the store, there is a difference however.
Your code
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
creates a selector for the cache entry that is created calling useGetProjectsQuery() - if you wanted the cache entry for useGetProjectsQuery("foo"), that would need to be projectsApiSlice.endpoints.getProjects.select("foo").
Please note that there should almost never be any reason to use those selectors with React components - those are an escape hatch if you are not working with React. If you are working with React, use the useGetProjectsQuery hook with selectFromResult.
I am seeing people use select in this fashion quite often recently and I assume this traces back to a tutorial that misunderstood the feature - did you learn that in a tutorial and could you share that tutorial? Maybe I can convince the author to change that part.

Observable from 2 promises

I think my solution is in this question but I can't get it to work Promise.all behavior with RxJS Observables?
I'm trying to return an observable on two promises via forkJoin.
One promise gets an ID from the server and another processes a file to generate a thumbnail.
export function createSceneFromFile(action$) {
return action$.ofType(CREATE_SCENE_FROM_FILE)
.mergeMap(({locationId,file}) =>
createSceneThumb(locationId,file)
.map((res,preview) => {
console.log(res,preview)
if (res.error) {
return { type: CREATE_SCENE_FAILED, payload: res.message }
} else {
return {type: CREATE_SCENE_SUCCESS, payload: {...res.payload,preview} }
}
})
.catch(err => { return { type: CREATE_SCENE_FAILED, message: err } })
)
}
function createSceneThumb(locationId,file){
const request = fetch(`${API_URL}/location/${locationId}/createscene/${file.name}/`, {
method: 'get',
credentials: 'include',
}).then(res => res.json())
const thumb = fileToScenePreview(file)
return Observable.forkJoin(request,thumb)
}
export function fileToScenePreview(file){
return new Promise((resolve,reject)=>{
getFileThumb(file).then((canvas)=> {
canvas.toBlob((blob) => {
blob.lastModifiedDate = new Date()
blob.name = 'scenepreview_' + file.name + '.png'
const uploader = new S3Upload({
getSignedUrl: getSignedUrl,
uploadRequestHeaders: {'x-amz-acl': 'public-read'},
contentType: 'image/png',
scrubFilename: (filename) => filename.replace(/[^\w\d_\-.]+/ig, ''),
contentDisposition: 'auto',
s3path: 'assets/',
onError:()=>reject,
onFinishS3Put: ()=>resolve(blob.name),
})
uploader.uploadFile(blob)
})
})
})
}
But i never get a response.
Is this the right way of going about it?

RxJs and redux-observable. Append value when ajax succeed or failed

I have the following epic:
export const changeTeamSubscriptionPlan = (braintreePlanId: string) => ({
type: CHANGE_TEAM_SUBSCRIPTION_PLAN,
braintreePlanId,
});
export const changeTeamSubscriptionPlanEpic = (action$: any, store: Store<ReduxState, *>) =>
action$.ofType(CHANGE_TEAM_SUBSCRIPTION_PLAN)
.mergeMap(({ braintreePlanId }) => {
const state = store.getState();
const { subscription_id } = state.payment.subscription;
let request;
if (subscription_id) {
request = ajax(api.changeTeamSubscriptionPlan(subscription_id, braintreePlanId));
} else {
const [method] = state.payment.paymentMethods;
request = ajax(api.createSubscription(braintreePlanId, method.token));
}
// I would like to emit another value ({ type: FETCH_TEAM }) no matter what happens
//(basically I try to invalidate the team data even if the request fails)
return request
.map(response => ({
type: CHANGE_TEAM_SUBSCRIPTION_PLAN + SUCCESS,
payload: {
data: response.response,
status: response.status,
},
}))
.catch(error => Observable.of({
type: CHANGE_TEAM_SUBSCRIPTION_PLAN + FAILURE,
response: {
data: error.xhr.response,
status: error.xhr.status,
},
}));
});
What I want to do is no matter if ajax call ends with catch or calls map I want to append another value.
I run out of ideas, so I'm hoping for help.
After switching to original operators it turned out that I can do this:
return request
.map(response => ({
type: CHANGE_TEAM_SUBSCRIPTION_PLAN + SUCCESS,
payload: {
data: response.response,
status: response.status,
},
}))
.catch(error => Observable.of({
type: CHANGE_TEAM_SUBSCRIPTION_PLAN + FAILURE,
error: mapToError(error),
}))
.concat(Observable.of({ type: 'CHUJ_TYPE' }));
and concat will append value even when the catch block fires.
I was originally using custom operator which I though will work just like catch does but will reduce boilerplate in my app:
Observable.prototype.mapFailure = function catchAndMapError(ACTION_TYPE, optionalPayload = {}) {
return Observable.create(subscriber =>
this.subscribe(
value => subscriber.next(value),
(error) => {
try {
const action = {
type: ACTION_TYPE + FAILURE,
error: {
...mapToError(error),
...optionalPayload,
},
};
subscriber.next(action);
} catch (e) { // catch mappings error
subscriber.error(e);
}
},
() => subscriber.complete(),
),
);
};
It seems like it doesn't work the same as catch.....

Chaining asynchronous actions with redux-api-middleware

I have a React app using Redux, redux-thunk, react-router and redux-api-middleware.
In order for a Page component in my app to render, I must access two API endpoints and store their results in the redux store.
Currently, I am dispatching both actions at the same time. This leads to problems when the results of one action complete before the other.
So I need to make sure one action completes before I dispatch the second one.
After reading all the documentation I am having trouble understanding how to make this happen with redux-api-middleware.
This is my current component:
class Page extends React.Component {
static prepareState = (dispatch: Function, match: Match) => {
const { params: { projectSlug, menuSlug, pageSlug, env } } = match
if (!projectSlug || !env) {
return
}
dispatch(getSettings(projectSlug, env))
dispatch(getPage(projectSlug, env, menuSlug || '', pageSlug || ''))
}
...
I have tried:
class Page extends React.Component {
static prepareState = (dispatch: Function, match: Match) => {
const { params: { projectSlug, menuSlug, pageSlug, env } } = match
if (!projectSlug || !env) {
return
}
dispatch(
dispatch(getSettings(projectSlug, env))
).then(res => {
console.log('success', res)
dispatch(getPage(projectSlug, env, menuSlug || '', pageSlug || ''))
}).catch(e => {
console.log('failure', e, menuSlug, pageSlug)
})
}
...
But this leads to the catch being called instead of the then. However the GET_SETTINGS_SUCCESS action is in fact completed, so I am confused as to why the catch is being called at all. What am I doing wrong?
This is the action generator:
export const getSettings = (projectSlug: string, env: string) => (dispatch: Function, getState: Function) => {
if (getState().settings[env] && loadCompleted(getState().settings.apiState)) {
// already loaded
return
}
dispatch({
[CALL_API]: {
endpoint: settingsUrl(projectSlug, env),
method: 'GET',
credentials: 'include',
types: [{
type: 'GET_SETTINGS',
meta: {projectSlug, env}
}, {
type: 'GET_SETTINGS_SUCCESS',
meta: {projectSlug, env}
}, {
type: 'GET_SETTINGS_FAIL',
meta: {projectSlug, env}
}]
}
})
}
Create a third action called getSettingsThenPage.
export const getSettingsThenPage = (
projectSlug,
env,
menuSlug = "",
pageSlug = ""
) => dispatch => {
return dispatch(getSettings(projectSlug, env)).then(() => {
return dispatch(getPage(projectSlug, env, menuSlug, pageSlug));
});
};
Invoke that action wherever you must.

Redux observable cancel next operator execution?

I am using redux-observable with redux for async actions. Inside epic's map operator i am doing some pre processing because its the central place.
My app calling same action from multiple container components with different values.
So basically i have to cancel my ajax request/next operator execution if deepEqual(oldAtts, newAtts) is true
code -
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(function(action) {
let oldAtts = store.getState().catalog.filterAtts
let newAtts = Object.assign({}, oldAtts, action.atts)
if (deepEqual(oldAtts, newAtts)) {
// Don't do new ajax request
}
const searchString = queryString.stringify(newAtts, {
arrayFormat: 'bracket'
})
// Push new state
pushState(newAtts)
// Return new `action` object with new key `searchString` to call API
return Object.assign({}, action, {
searchString
})
})
.mergeMap(action =>
ajax.get(`/products?${action.searchString}`)
.map(response => doFetchProductsFulfilled(response))
.catch(error => Observable.of({
type: FETCH_PRODUCTS_FAILURE,
payload: error.xhr.response,
error: true
}))
.takeUntil(action$.ofType(FETCH_PRODUCTS_CANCEL))
);
}
Not sure whether its right way to do it from epic.
Thanks in advance.
You can do this:
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(action => ({
oldAtts: store.getState().catalog.filterAtts,
newAtts: Object.assign({}, oldAtts, action.atts)
}))
.filter(({ oldAtts, newAtts }) => !deepEqual(oldAtts, newAtts))
.do(({ newAtts }) => pushState(newAtts))
.map(({ newAtts }) => queryString.stringify(newAtts, {
arrayFormat: 'bracket'
}))
.mergeMap(searchString => ...);
}
But most likely you do not need to save the atts to state to do the comparison:
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(action => action.atts)
.distinctUntilChanged(deepEqual)
.map(atts => queryString.stringify(atts, { arrayFormat: 'bracket' }))
.mergeMap(searchString => ...);
}

Resources