Error using redux saga, take(patternOrChannel): argument 8 is not valid channel or a valid pattern - javascript

I'm trying to use redux saga library to capture some actions but I'm having this error when I run the app:
index.js:2178 uncaught at rootSaga at rootSaga at projectSaga at
watchFetchRequest at takeEvery Error: take(patternOrChannel):
argument 8 is not valid channel or a valid pattern
at take (http://localhost:3000/static/js/bundle.js:84689:9)
at takeEvery (http://localhost:3000/static/js/bundle.js:85993:94)
at createTaskIterator (http://localhost:3000/static/js/bundle.js:85179:17)
at runForkEffect (http://localhost:3000/static/js/bundle.js:85583:24)
at runEffect (http://localhost:3000/static/js/bundle.js:85468:872)
at next (http://localhost:3000/static/js/bundle.js:85348:9)
at proc (http://localhost:3000/static/js/bundle.js:85303:3)
at runForkEffect (http://localhost:3000/static/js/bundle.js:85587:19)
at runEffect (http://localhost:3000/static/js/bundle.js:85468:872)
at http://localhost:3000/static/js/bundle.js:85677:14
at Array.forEach ()
at runAllEffect (http://localhost:3000/static/js/bundle.js:85676:10)
at runEffect (http://localhost:3000/static/js/bundle.js:85468:413)
at next (http://localhost:3000/static/js/bundle.js:85348:9)
at proc (http://localhost:3000/static/js/bundle.js:85303:3)
at runForkEffect (http://localhost:3000/static/js/bundle.js:85587:19)
at runEffect (http://localhost:3000/static/js/bundle.js:85468:872)
at http://localhost:3000/static/js/bundle.js:85677:14
at Array.forEach ()
at runAllEffect (http://localhost:3000/static/js/bundle.js:85676:10)
at runEffect (http://localhost:3000/static/js/bundle.js:85468:413)
at next (http://localhost:3000/static/js/bundle.js:85348:9)
at proc (http://localhost:3000/static/js/bundle.js:85303:3)
at runSaga (http://localhost:3000/static/js/bundle.js:85858:76)
at Object../src/store/ReduxRoot.tsx (http://localhost:3000/static/js/bundle.js:95823:16)
at webpack_require (http://localhost:3000/static/js/bundle.js:679:30)
at fn (http://localhost:3000/static/js/bundle.js:89:20)
at Object../src/index.tsx (http://localhost:3000/static/js/bundle.js:95325:75)
at webpack_require (http://localhost:3000/static/js/bundle.js:679:30)
at fn (http://localhost:3000/static/js/bundle.js:89:20)
at Object.0 (http://localhost:3000/static/js/bundle.js:96424:18)
at webpack_require (http://localhost:3000/static/js/bundle.js:679:30)
at ./node_modules/#babel/runtime/helpers/arrayWithoutHoles.js.module.exports
(http://localhost:3000/static/js/bundle.js:725:37)
at http://localhost:3000/static/js/bundle.js:728:10
This is the saga code:
import { all, call, fork, put, takeEvery } from 'redux-saga/effects';
import { ActionType, Action, SearchCriteria } from '../model/types';
import { searchProjectsError, searchProjectsCompleted } from '../actions/projects';
import { API_URL } from '../../config';
// import axios from 'axios';
function callApi(method: string, url: string, path: string, data?: any) {
return fetch(url + path, {
method,
headers: {'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*'},
body: JSON.stringify(data)
}).then(res => res.json());
}
// Here we use `redux-saga` to trigger actions asynchronously. `redux-saga` uses something called a
// "generator function", which you can read about here:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
function* handleFetch(action: Action<SearchCriteria>) {
try {
// To call async functions, use redux-saga's `call()`.
const res = yield call(callApi, 'get', API_URL , '/api/DocumentLoader/GetProjects/', action.payload);
if (res.error) {
yield put(searchProjectsError(res.error));
} else {
yield put(searchProjectsCompleted(res));
}
} catch (err) {
if (err instanceof Error) {
yield put(searchProjectsError(err.stack!));
} else {
yield put(searchProjectsError('An unknown error occured.'));
}
}
}
// This is our watcher function. We use `take*()` functions to watch Redux for a specific action
// type, and run our saga, for example the `handleFetch()` saga above.
function* watchFetchRequest() {
yield takeEvery(ActionType.SEARCH_PROJECTS, handleFetch); // I think the error is here
}
// We can also use `fork()` here to split our saga into multiple watchers.
function* projectSaga() {
yield all([fork(watchFetchRequest)]);
}
export default projectSaga;
I already tried to find an answer here in SO, but the only I could find was this post, but ActionType is been exported. I think the problem is with the parameter of handleFetch function
This is the action:
export function searchProjects(criterias: SearchCriteria): Action<SearchCriteria> {
return {
type: ActionType.SEARCH_PROJECTS,
payload: criterias
};
}
Another thing it could be is maybe I did something wrong at the time to compose the saga middleware:
const sagaMiddleware = createSagaMiddleware();
var middleware = applyMiddleware(logger, thunk, sagaMiddleware);
if (process.env.NODE_ENV === 'development') {
middleware = composeWithDevTools(middleware);
}
// Here we use `redux-saga` to trigger actions asynchronously. `redux-saga` uses something called a
// "generator function", which you can read about here:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
export function* rootSaga() {
yield all([fork(projectSaga)]);
}
// running the root saga, and return the store object.
sagaMiddleware.run(rootSaga);
I also tried removing the action parameter from handleFetch and the error is still happening

I found what was the error, the problem was the definition of ActionType enum. It was without assigning an string value for each action.
export const enum ActionType {
// Projects
SEARCH_PROJECT,
SEARCH_PROJECTS_COMPLETED,
SEARCH_PROJECTS_ERROR,
}
This fixed the problem:
export const enum ActionType {
// Projects
SEARCH_PROJECTS= '##projects/SEARCH_PROJECTS',
SEARCH_PROJECTS_COMPLETED= '##projects/SEARCH_PROJECTS_COMPLETED',
SEARCH_PROJECTS_ERROR= '##projects/SEARCH_PROJECTS_ERROR',
}

The only possible error could be this:
Check whether you have imported the { ActionType } object here, from the models file where you have defined your constants.
export function searchProjects(criterias: SearchCriteria):
Action<SearchCriteria> {
return {
type: ActionType.SEARCH_PROJECTS,
payload: criterias
};
}

Related

Type error with react-query and UseQueryResult

I am using react-query in my TS project:
useOrderItemsForCardsInList.ts:
import { getToken } from '../../tokens/getToken';
import { basePath } from '../../config/basePath';
import { getTokenAuthHeaders } from '../../functions/sharedHeaders';
import { useQuery } from 'react-query';
async function getOrderItemsForCardsInList(listID: string) {
const token = await getToken();
const response = await fetch(`${basePath}/lists/${listID}/order_items/`, {
method: 'GET',
headers: getTokenAuthHeaders(token)
});
return response.json();
}
export default function useOrderItemsForCardsInList(listID: string) {
if (listID != null) {
return useQuery(['list', listID], () => {
return getOrderItemsForCardsInList(listID);
});
}
}
I use my query result over here:
import { useCardsForList } from '../../hooks/Cards/useCardsForList';
import useOrderItemsForCardsInList from '../../hooks/Lists/useOrderItemsForCardsInList';
import usePaginateCardsInList from '../../hooks/Cards/usePaginateCardsInList';
export default function CardsListFetch({ listID }: { listID: string }) {
const { isLoading, isError, error, data } = useCardsForList(listID);
const { orderItems } = useOrderItemsForCardsInList(listID);
const pagesArray = usePaginateCardsInList(orderItems, data);
return (
...
);
}
However, on my const { orderItems } = useOrderItemsForCardsInList(listID); line, I get the following error:
Property 'orderItems' does not exist on type 'UseQueryResult<any, unknown> | undefined'.
How can I resolve this? I don't really know how to consume the result of my query on Typescript, any help is be appreciated
The property on useQuery that you need to consume where you find your data is called data, so it should be:
const { data } = useOrderItemsForCardsInList(listID);
if that data has a property called orderItems, you can access it from there.
However, two things I'm seeing in your code:
a conditional hook call of useQuery (which is forbidden in React)
your queryFn returns any because fetch is untyped, so even though you are using TypeScript, you won't get any typesafety that way.
If you are using React with react-query, I suggest you install this devDependency called: "#types/react-query". When using VS Code or any other smart text editor that will help, because it will help you with type suggestions.
npm i --save-dev #types/react-query
Then go to your code and fix couple things:
remove condition from useOrderItemsForCardsInList(). Don’t call React Hooks inside loops, conditions, or nested functions. See React hooks rules.
import UseCategoryResult and define interface for your return object. You can call it OrderItemsResult or similar. Add type OrderType with the fields of the order object or just use orderItems: any for now.
Add return type UseQueryResult<OrderItemsResult> to useOrderItemsForCardsInList() function.
Fix return value of getOrderItemsForCardsInList(), it should not be response.json() because that would be Promise, not actual data. Instead use await response.json().
So your function useOrderItemsForCardsInList() will return UseQueryResult which has properties like isLoading, error and data. In your second code snipper, you already use data in one place, so instead rename data to orderData and make sure you define default orderItems to empty array to avoid issues: data: orderData = {orderItems: []}
useOrderItemsForCardsInList.ts:
import { getToken } from '../../tokens/getToken';
import { basePath } from '../../config/basePath';
import { getTokenAuthHeaders } from '../../functions/sharedHeaders';
import { useQuery, UseCategoryResult } from 'react-query';
type OrderType = {
id: string;
name: string;
// whatever fields you have.
}
interface OrderItemsResult {
orderItems: OrderType[],
}
async function getOrderItemsForCardsInList(listID: string) {
const token = await getToken();
const response = await fetch(`${basePath}/lists/${listID}/order_items/`, {
method: 'GET',
headers: getTokenAuthHeaders(token)
});
const data = await response.json();
return data;
}
export default function useOrderItemsForCardsInList(listID: string): UseQueryResult<OrderItemsResult> {
return useQuery(['list', listID], () => {
return getOrderItemsForCardsInList(listID);
});
}
Use your query result:
import { useCardsForList } from '../../hooks/Cards/useCardsForList';
import useOrderItemsForCardsInList from '../../hooks/Lists/useOrderItemsForCardsInList';
import usePaginateCardsInList from '../../hooks/Cards/usePaginateCardsInList';
export default function CardsListFetch({ listID }: { listID: string }) {
const { isLoading, isError, error, data } = useCardsForList(listID);
const { data: orderData = { orderItems: []}} = useOrderItemsForCardsInList(listID);
const pagesArray = usePaginateCardsInList(orderItems, data);
return (
...
);
}

Cannot read property of undefined, Typescript

I'm having issues with creating a class function with optional parameters. I'm plagued with the following error and since I'm a new comer to typescript I'm not really sure as to what I have to change in my code to fix it. This code works perfectly fine in an earlier project written in JS only.
It specifically highlights the getRequestOptions, when I go check it in base-service.ts in the browser error.
08:32:12.755 client.ts:22 [vite] connecting...
08:32:13.067 client.ts:52 [vite] connected.
08:32:17.043 locales-service.ts:7 getting locales
08:32:17.048 base-service.ts:19 get /Api/GetLocales/ undefined undefined undefined
08:32:17.288 base-service.ts:21 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'getRequestOptions')
at request (base-service.ts:21)
at getLocales (locales-service.ts:9)
at setup (locale-selector.vue:22)
at callWithErrorHandling (runtime-core.esm-bundler.js:6737)
at setupStatefulComponent (runtime-core.esm-bundler.js:6346)
at setupComponent (runtime-core.esm-bundler.js:6302)
at mountComponent (runtime-core.esm-bundler.js:4224)
at processComponent (runtime-core.esm-bundler.js:4199)
at patch (runtime-core.esm-bundler.js:3791)
at mountChildren (runtime-core.esm-bundler.js:3987)
And my code
base-service.ts
import axios from '#/plugins/axios'
export default class BaseService {
getRequestOptions(url:any, method:any, data?:any, params?:any , customHeaders?:any) {
const headers = {
...customHeaders
}
return {
url:url,
method:method,
data: data,
params: params,
headers: headers,
}
}
async request(method:any, url:any, data?:any, params?:any, headers?:any) {
console.log(method,url,data,params,headers)
const options = this.getRequestOptions(method, url, data, params, headers)
try {
console.log(options)
const response = await axios(options)
console.log("Getting response from api")
console.log(response)
if (response.status === 200) {
return response.data
}
} catch (error) {
console.log(error)
}
}
}
locales-service.ts
import BaseService from '../base-service'
//?Import models in the future
//import { } from '#/common/models'
class LocaleServ extends BaseService {
async getLocales() {
console.log("getting locales")
let result = await super.request(
'get',
'/Api/GetLocales/'
)
console.log(result)
return result
}
}
export const LocaleService = new LocaleServ()
Any help would be greatly appreciated
Edit:
<script lang="ts">
import { ref } from 'vue'
import { defineComponent } from 'vue'
import CountryFlag from 'vue-country-flag-next'
import { LocaleService } from '#/services'
export default defineComponent({
name: 'LocaleSelector',
components: {
CountryFlag
},
setup () {
const { getLocales } = LocaleService
const data = getLocales()
return {
data:data
}
}
})
</script>
And then in the component I call it with {{data}}
The issue lied in the way I called the destructured getLocales() method from LocaleService in locale-service.vue
I noticed that this was undefined after attaching a debugger. According to this article, destructuring it caused it to lose it's context. Therefore when I called getLocales() in base-service.ts, this was undefined.
Simple work around to fix the, caused due to inexperience, problem was to turn.
const { getLocales } = LocaleService
const data = getLocales()
into
const data = LocaleService.getLocales()

Typescript Error with Axios API response - array methods do not exist on response

I am having an issue with an axios response and a typescript error -
The Error that i am getting is Property 'filter' does not exist on type 'ISearchServiceResponse'.
How do I tell typescript that the object that i want to work with is an array and not the axios response after it has come back from the server?
useEffect(() => {
if (apiKey) {
const getUserFilters = async () => {
try {
const res = await searchService.filters.get(apiKey)
const responsesWithoutKeywordAsset = res.data
.filter(item => !item.key.includes('BAR'))
.filter(other => !other.key.includes('FOO'))
} catch (err) {
console.error(err)
}
}
getUserFilters()
}
}, [apiKey])
type Res<T> = Promise<AxiosResponse<T>>
export const searchService = {
filters: {
get: (apiKey: string): Res<SearchService.ISearchServiceResponse> =>
axiosInstance.get(
www.myapicall.com?aggregation=PAYLOAD_CATEGORY`,
{
headers: {
'X-Api-Key': apiKey
}
})
}
}
export namespace SearchService {
export interface ISearchServiceResponse {
data: Filters[]
}
export interface Filters {
key:string
value: string
}
}
General advice
I've personally never defined my interfaces directly using AxiosResponse.
Instead, I've made use of the generic methods in the Axios typings.
For example:
const response = axios.get<User[]>("/users");
In this case, response will have a type of AxiosResponse<User[]>.
Meaning that response.data has a type of User[], which will have a filter-function.
Your specific issue
If I read your code well, then Res<SearchService.ISearchServiceResponse> is equal to Promise<AxiosResponse<SearchService.ISearchServiceResponse>>.
If you unwrap the promise, then you're left with AxiosResponse<SearchService.ISearchServiceResponse>.
This means that AxiosResponse has a field data of type SearchService.ISearchServiceResponse.
This data, also has a data field as defined by your interface.
So a call to response.data.data.filter would work.
But I'd change the interface into something that makes more sense.
Suggestion from comments
It was suggested in the comments (by #marita-farruggia) to let ISearchServiceResponse extend from AxiosResponse which would work.
However, I'd define it as follows:
interface ISearchServiceResponse extends AxiosResponse<Filters[]> {}
Note: you'd then need to change your definition of Res<T> to Promise<T> (which makes the Res type an unnecessary abstraction of Promise in my opinion).

Redux-Saga failing error handling tests using Generator.prototype.next()

I am visiting an old project and ran the tests. However, testing my Redux-Saga is not passing its error handling tests.
All The other tests are passing without problem so I know the functions are being called correctly. Below is the test file
import { takeLatest, call, put } from 'redux-saga/effects'
import { firestore, convertCollectionsSnapshotToMap } from '../../firebase/firebase.utils'
import { fetchCollectionsSuccess, fetchCollectionsFailure } from './shop.actions'
import ShopActionTypes from './shop.types'
import { fetchCollectionsAsync, fetchCollectionsStart } from './shop.sagas'
describe('fetch collections start saga', () => {
it('should trigger on FETCH_COLLECTIONS_START', () => {
const generator = fetchCollectionsStart()
expect(generator.next().value).toEqual(
takeLatest(ShopActionTypes.FETCH_COLLECTIONS_START, fetchCollectionsAsync)
)
})
})
describe('fetch collections async saga', () => {
const generator = fetchCollectionsAsync()
it('should call firestore collection ', () => {
const getCollection = jest.spyOn(firestore, 'collection')
generator.next()
expect(getCollection).toHaveBeenCalled()
})
it('should call convertCollectionsSnapshot saga ', () => {
const mockSnapshot = {}
expect(generator.next(mockSnapshot).value).toEqual(
call(convertCollectionsSnapshotToMap, mockSnapshot)
)
})
it('should fire fetchCollectionsSuccess if collectionsMap is succesful', () => {
const mockCollectionsMap = {
hats: { id: 1 }
}
expect(generator.next(mockCollectionsMap).value).toEqual(
put(fetchCollectionsSuccess(mockCollectionsMap))
)
})
// THIS IS THE FAILING CODE
it('should fire fetchCollectionsFailure if get collection fails at any point', () => {
const newGenerator = fetchCollectionsAsync()
newGenerator.next()
expect(newGenerator.throw({ message: 'error' }).value).toEqual(
put(fetchCollectionsFailure('error'))
)
})
})
Below is my shop Saga.js file
import { takeLatest, call, put, all } from 'redux-saga/effects'
import { firestore, convertCollectionsSnapshotToMap } from '../../firebase/firebase.utils'
import { fetchCollectionsSuccess, fetchCollectionsFailure } from './shop.actions'
import ShopActionTypes from './shop.types'
export function* fetchCollectionsAsync() {
try {
const collectionRef = firestore.collection('collections')
const snapshot = yield collectionRef.get()
const collectionsMap = yield call(convertCollectionsSnapshotToMap, snapshot)
yield put(fetchCollectionsSuccess(collectionsMap))
} catch (error) {
yield put(fetchCollectionsFailure(error.message))
}
}
export function* fetchCollectionsStart() {
yield takeLatest(ShopActionTypes.FETCH_COLLECTIONS_START, fetchCollectionsAsync)
}
export function* shopSagas() {
yield all([call(fetchCollectionsStart)])
}
I have tried mocking the error and passing it into my tests also but I am getting the same output in my tests below, no matter how I refactor the code.
PASS src/pages/checkout/checkout.test.js
FAIL src/redux/shop/shop.sagas.test.js
● fetch collections async saga › should fire fetchCollectionsFailure if get collection fails at any point
error
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 5 passed, 6 total
Snapshots: 1 passed, 1 total
Time: 3.076 s
Ran all test suites related to changed files
If anyone has any advice on where to look and how to fix this, it would be greatly appreciated. Thanks in advance
I have written the following code as a workaround. Tests are now passing and the fetchCollectionsFailure is being called. It's not the prettiest code but it's a workaround and fulfills the criteria for now.
it('should fire fetchCollectionsFailure if get collection fails at any point', () => {
const newGenerator = fetchCollectionsAsync()
const error = {
'##redux-saga/IO': true,
combinator: false,
payload: {
action: {
payload: "Cannot read property 'get' of undefined",
type: 'FETCH_COLLECTIONS_FAILURE'
},
channel: undefined
},
type: 'PUT'
}
expect(newGenerator.next().value).toMatchObject(error)
})

javascript - call exported function from another class

Here's the scenario: There is a file called api.js which has method api() to make api calls. There is another class called AutoLogout which has functionality to show autologout modal and logging out user after certain time in case of no activity. These works fine.
index.js in ../services
export { default as api } from './api';
// export { api, onResponse } from './api'; tried this as well
export { default as userService } from './userService';
api.js
import userService from './userService';
export function onResponse(response) {
// returns response to calling function
return response;
}
async function api(options) {
const settings = Object.assign(
{
headers: {
'content-type': 'application/json',
'x-correlation-id': Math.random()
.toString(36)
.substr(2),
},
mode: 'cors',
credentials: 'include',
body: options.json != null ? JSON.stringify(options.json) : undefined,
},
options,
);
const response = await window.fetch(`/api/v0${options.endpoint}`, settings);
// calling onResponse() to send the response
onResponse(response);
if (response.status === 403) return userService.logout();
if (response.status > 299) throw new Error();
if (response.status === 204) return true;
return response.json ? response.json() : false;
}
export default api;
Now, in response header I've "x-expires-at" and I want to use it in autologout. So, that if api call is made the user token resets.
auto-lougout.js
import { userService, api } from '../services';
// import { userService, api, onResponse } from '../services'; tried this as well
export default class AutoLogout {
constructor() {
super();
if (!userService.getUser()) userService.logout();
// here I am not able to call onResponse() from api.js
// getting response received from onResponse()
api.onResponse((resp) => { console.log(resp.headers.get('x-expires-at'))});
}
}
Trying to implement as an example given in this article:
https://zpao.com/posts/calling-an-array-of-functions-in-javascript/
Here I cannot use export { api, onResponse }; as api is already being used at multiple places in whole project.
How do I call onResponse function in one js file from another class in another js file ? Am I using callback correctly here ? If not, how to use callback correctly in such scenario ?
Here I cannot use export { api, onResponse }; as api is already being used at multiple places in whole project.
The correct import/export syntax for your project would be
// api.js
export function onResponse() { … }
export default function api() { … }
// index.js
export { default as userService } from './userService';
export { default as api, onResponse } from './api';
// elsewhere
import { userService, api, onResponse } from '../services';
// use these three
Am I using callback correctly here?
No, not at all. onResponse should not be a function declared in your api.js file, and it should not be exported from there - sparing you all the above hassle.
If not, how to use callback correctly in such scenario?
Make the callback a parameter of the function that uses it:
export default async function api(options, onResponse) {
// ^^^^^^^^^^
const settings = Object.assign(…);
const response = await window.fetch(`/api/v0${options.endpoint}`, settings);
onResponse(response);
…
}
Then at the call of the api function, pass your callback as an argument:
api(options, resp => {
console.log(resp.headers.get('x-expires-at'));
});

Categories

Resources