I'm trying to apply the ajax method posted here: https://github.com/redux-observable/redux-observable/blob/master/docs/basics/Epics.md
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);
// later...
dispatch(fetchUser('torvalds'));
When trying this method, I get the message:
TypeError: Object(...)(...).pipe is not a function
So pipe doesn't appear to exist. (It concerns the second pipe after the ajax call).
How do I fix this?
I installed the following dependencies:
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
Edit:
I changed my code to ajax.get and the calling code:
export const fetchTrendingEpic = action$ => action$.pipe(
ofType(FETCH_TRENDING_REQUEST),
mergeMap(async(action) => {
const res = await fetchPostStats(action.payload);
console.log(res);
res.pipe(
map(response => {
console.log('response', response);
setTrendingPlaces({trendingPlaces: response});
})
)
})
);
The res was properly printed (showing an observable), but now I get an error:
TypeError: Cannot read property 'type' of undefined
This is how I create my store in dev:
const createEnhancer = (epicMiddleware) => {
const middleware = [ epicMiddleware, createLogger() ];
let enhancer;
if (getEnvironment() === 'development') {
enhancer = composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
);
};
export default (initialState) => {
const epicMiddleware = createEpicMiddleware();
const enhancer = createEnhancer(epicMiddleware);
const store = createStore(rootReducer, initialState, enhancer);
epicMiddleware.run(rootEpic);
return store;
}
Edit note:
This is code executed in NodeJS (SSR).
I'm struggling with this, don't really understand how this can be so hard to get working without error.
Can't quite see how the example code will ever work when ajax.getJSON returns a promise, not an Observable...
My service that called the ajax request was marked with async because the previous implementation used Axios before the refactoring.
This was the reason why my console logged a promise as return value of the function.
Removing async returns the Observable as expected.
As #Dez mentioned in the comments, there is also need to add return values.
And last but not least, rxjs ajax does not work in a NodeJS environment.
Therefore, based on this thread:
https://github.com/ReactiveX/rxjs/issues/2099
I have found that someone created the following npm package that I will try out shortly: https://github.com/mcmunder/universal-rxjs-ajax
Axios works fine with rxjs in nodejs, just doing something like this...
const { from } = require('rxjs');
const { map } = require('rxjs/operators');
const axios = require('axios');
const responsePromise = axios.get('https://jsonplaceholder.typicode.com/todos/1');
const response$ = from(responsePromise);
response$
.pipe(
map(response => ({ type: 'RESPONSE_RECEIVED', payload: response.data}))
)
.subscribe(console.log);
Related
Hopefully a simply one.
I make an API call in my component which brings down some account information such as AccountUid, Category etc, i use state to set these.
useEffect(() => {
fetch(feed_url, {
headers: {
//Headers for avoiding CORS Error and Auth Token in a secure payload
"Access-Control-Allow-Origin": "*",
Authorization: process.env.REACT_APP_AUTH_TOKEN,
},
})
//Return JSON if the Response is recieved
.then((response) => {
if (response.ok) {
return response.json();
}
throw response;
})
//Set the Account Name state to the JSON data recieved
.then((accountDetails) => {
setAccountDetails(accountDetails);
console.log(accountDetails.accounts[0].accountUid);
console.log(accountDetails.accounts[0].defaultCategory);
})
//Log and Error Message if there is an issue in the Request
.catch((error) => {
console.error("Error fetching Transaction data: ", error);
});
}, [feed_url]);
This Works perfectly well and it Logs the correct values in my .then when testing it.
The issue however is that i want to pass these down as props. But i get an error that they are being returned as null (My default state).. i presume as they're jumping ahead.
<div className="App">
<GetAccountName
accountUID={accountDetails.accounts[0].accountUID}
defCategory={accountDetails.accounts[0].defaultCategory}
/>
</div>
How do i pass the the 2 details im logging as props?? I've tried setting default state to "" instead of null and just get that it is undefined.
If you dont want to use conditional render in your child component, so you should try optional chaining
<GetAccountName
accountUID={accountDetails?.accounts?.[0]?.accountUID}
defCategory={accountDetails?.accounts?.[0]?.defaultCategory}
/>
Since fetching is asyncronous, the most common way is to show some loading indicator (like a spinner) & once the data come in, show the component instead.
If you don't need an indicator, you might just return null.
The general idea is to manipulate some intermediary states (e.g. data, isError) based on the promise state.
Check out react-query library example or a lighter abstraction like useFetch hook to see how they manage it.
Here's a sample implementation of useFetch taken from this article:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState(() => {});
React.useEffect(() => {
const fetchData = async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, {...options, signal});
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
}
}, []);
return { response, error, abort };
};
I'm using react-query to make two separate queries in the same React component. I originally tried using two useQuery hooks:
export default function Component() {
const [barData, setBarData] = useState();
const [lineData, setLineData] = useState();
const { error: errorBar, isLoading: loadingBar } = useData(
"barQuery",
"BAR_TEST_SINGLE",
setBarData
);
const { error: errorLine, isLoading: loadingLine } = useData(
"lineQuery",
"LINE_TEST",
setLineData
);
const isLoading = loadingLine && loadingBar;
const error = errorLine && errorBar;
if (isLoading) return <LoadingSpinner title={title} />;
if (error)
return <InvalidStateAPI description={error.message} title={title} />;
return (
<>
<Line data={lineData} />
<Bar data={barData} />
</>
);
}
Here's the content of useData, which is imported from another file:
export function useData(queryId, chartId, setData) {
return useQuery(
queryId,
() =>
fetchData(chartId, "models").then((resp) => {
setData(resp.Item.data);
})
);
}
fetchData is a function that queries an AWS DDB table. I'm happy to post that function, but I'm currently excluding it for brevity.
Rendering Component results in this error: TypeError: Cannot read properties of undefined (reading 'filter'). I suspect this error is thrown because a component doesn't get its data in time for rendering. I thought this would be solved by adding const isLoading = loadingLine && loadingBar;, as suggested in this post, but it did not.
Finally, on the recommendation of this SO answer, I decided to use useQueries. The documentation is quite sparse, but it recommends the following format:
const results = useQueries([
{ queryKey: ['post', 1], queryFn: fetchPost },
{ queryKey: ['post', 2], queryFn: fetchPost },
])
I modified my original code to the following:
const results = useQueries([
{
queryKey: ["post", 1],
queryFn: useData2("barQuery", "BAR_TEST_SINGLE", setBarData),
},
{
queryKey: ["post", 2],
queryFn: useData2("lineQuery", "LINE_TEST", setLineData),
},
]);
but now I'm getting this error: TypeError: _this2.options.queryFn is not a function.
Am I formatting my query incorrectly? How can I fix it? Alternatively, are there other ways to run different queries using react-query in the same component?
No, this is not the correct syntax for useQueries. You can't pass a useQuery hook in as queryFn - the queryFn needs the function that fetches the data, in your case, that would be fetchData(chartId, "models").
The root cause of your initial problem however seems to be that your condition only waits until one of the queries has finished loading:
const isLoading = loadingLine && loadingBar;
if loadingLine is false and loadingBar is true, this condition will yield false and you will thus not display a loading spinner anymore. If you want to wait until all queries have finished loading, that would be:
const isLoading = loadingLine || loadingBar;
lastly, I'd like to point out that copying data from react-query to local state is not necessary. react-query will return the result returned from the queryFn as the data field. My implementation would look something like that:
export function useData(queryId, chartId) {
return useQuery(
[queryId, chartId],
() => fetchData(chartId, "models")
);
}
const { error: errorBar, isLoading: loadingBar, data: barData } = useData(
"barQuery",
"BAR_TEST_SINGLE",
);
const { error: errorLine, isLoading: loadingLine, data: lineData } = useData(
"lineQuery",
"LINE_TEST",
);
const isLoading = loadingLine || loadingBar;
or, alternatively, with useQueries:
const results = useQueries([
{
queryKey: ["barQuery", "BAR_TEST_SINGLE"],
queryFn: () => fetchData("BAR_TEST_SINGLE", "models")
},
{
queryKey: ["lineQuery", "LINE_TEST"],
queryFn: () => fetchData("LINE_TEST", "models")
},
]);
const isLoading = results.some(result => result.isLoading)
#TKDo, Your solution only works with react-query versions till "4.0.0-alpha.1", the issue will crop up again, if we use any latest version of react-query ("4.0.0-alpha.2" and above). To fix that, we need to use an object and assign the list of queries as the value to queries key like below example.
const results = useQueries({queries: [
{
queryKey: ["barQuery", "BAR_TEST_SINGLE"],
queryFn: () => fetchData("BAR_TEST_SINGLE", "models")
},
{
queryKey: ["lineQuery", "LINE_TEST"],
queryFn: () => fetchData("LINE_TEST", "models")
}]});
React-Query V4 Migration docs for reference
I couldn't add a comment as my reputation is currently too low, but I had the same problem as vaibhav-deep, so thought I would put this here even if it's a bit off-topic.
It was fixed by returning whatever my data is in the async/await fetch function.
const queries = useQueries(
queryList.map((query) => {
return {
queryKey: [query],
queryFn: () => fetchData(query),
};
})
);
async function fetchData(query) {
const response = await axios.get(apiCall)...
...
return response.data;
}
I am using Redux Toolkit Query to fetch datas from Audius server. The service is based on many IPFS nodes, and it is best practice to make a query of the best performing servers to which send the API requests in that particular moment. This is the function Audius API docs suggest to use in order to find the right server:
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)]
var host = await fetch('https://api.audius.co')
.then(r => r.json())
.then(j => j.data)
.then(d => sample(d))
I need to get the url from this function and feed it into the function where I use the createApi method. I wrapped the function in an async function, since it uses await, but I am stuck in the Promise.
async function getHost() {
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)]
var host = await fetch('https://api.audius.co')
.then(r => r.json())
.then(j => j.data)
.then(d => sample(d))
}
I don't know how to get a value as result of this function, I know how to do it with React components, using useState Hook, but I would not like to use it here, as I think it will slow down the process. I tried to understand how to use the createAsyncThunk but I can't wrap my mind around it.
The rest of the code looks like this:
const contentProvider = `https://discoveryprovider2.audius.co`
const audiusVersion = `/v1`
const appName = `app_name=ZION`
const baseUrl = contentProvider + audiusVersion
const section = [`/users`, `/playlists`, `/tracks`]
const search = `/search?query=`
export const audiusApi = createApi({
reducerPath: 'audiusApi',
baseQuery: fetchBaseQuery({ baseUrl: `${baseUrl}` }),
endpoints: (builder) => ({
// SEARCH USERS
// https://discovery-a.mainnet.audius.radar.tech/v1/users/search?query=Brownies&app_name=EXAMPLEAPP
searchUsers: builder.query({
query: (searchQuery) => `${section[0]}${search}${searchQuery}${appName}`
}),
.........})
}),
})
export const {
useSearchUsersQuery,
useGetUserQuery,
useGetUsersFavTracksQuery,
useGetUsersRepostsQuery,
useGetUserMostUsedTagsQuery,
useGetUserTracksQuery,
useSearchPlaylistQuery,
useGetPlaylistQuery,
useGetPlaylistTracksQuery,
useSearchTracksQuery,
useTrendingTracksQuery,
useGetTrackQuery,
useStreamTrackQuery
} = audiusApi
So basically I need to place the result of the async function = to contentProvider.
I tried doing simply
var response = getHost() // my async function
var contentProvider = response
but this doesn't pass through
Hope someone can help me out with this one =).
So you want a baseQuery with a dynamic baseUrl.
We have an example on a baseQuery that uses Redux state for the baseUrl in the docs.
That still means, you have to get the baseUrl into the store though.
Adjusting an example from the Redux Essentials tutorial:
export const getHost = createAsyncThunk('host/getHost', async () => {
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)]
return fetch('https://api.audius.co')
.then(r => r.json())
.then(j => j.data)
.then(d => sample(d))
})
const hostSlice = createSlice({
name: 'host',
initialState: null,
reducers: {
},
extraReducers(builder){
builder.addCase(getHost.fulfilled, (state, action) => {
return action.payload
}
}
})
Then you plug your hostSlice.reducer into configureStore:
const store = configureStore({
reducer: {
host: hostSlice.reducer
// more stuff you had before
}
})
and dispatch the thunk:
dispatch(getHost())
Your host will be available via getState().host in your adjusted baseQuery then.
if I understand you well, I think you struggle with how to work with async functions.
you can easily use async await.
async function getHost() {
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)]
var host = await fetch('https://api.audius.co');
var json = await host.json();
var data = await json.data;
var d = await sample(data);
return d;
}
and you can call it as
var result = await getHost();
result.
then(d => console.log(d));
I have two components which i am working with. In the first component, i made a custom useEffect hook that retrieves data from my server. Please see the code below:
Code snippet One
import {useState, useCallback} from 'react';
import {stageQuizApi} from '../api/quiz';
import {QuestionService} from "../services/IdDbServices/question_service";
const usePostData = ({url, payload, config}) => {
const [res, setRes] = useState({data: null, error: null, isLoading: false});
const callAPI = useCallback(() => {
setRes(prevState => ({...prevState, isLoading: true}));
stageQuizApi.patch(url, payload, config).then( res => {
setRes({data: res.data, isLoading: false, error: null});
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
}).catch((error) => {
console.log(error)
if (error.response) {
const errorJson = error.response.data
setRes({data: null, isLoading: false, error: errorJson.message});
} else if (error.request) {
setRes({data: null, isLoading: false, eror: error.request});
} else {
setRes({data: null, isLoading: false, error: error.message});
}
})
}, [url, config, payload])
return [res, callAPI];
}
export default usePostData;
The above module has two purpose. It first makes an axios request to my endpoint and secondly makes a database insertion to browser IndexDb (similar to localstorage but with sql approach) ( like inserting data into the database using the response that was gotten from the first request. so typically i have a promise in the outer .then block. This part:
Code snippet Two
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
Here is the problem, I am trying to maintain state as i want the result of this module to be shared in another route and i don't want to hit the server again hence i inserted the result into indexDb browser storage. Here is the code that executes the above module:
Code snippet Three
const displaySingleQuestion = ()=>{
OnUserGetQuestion();
history.push('/player/question');
}
The above method is called from my first route /question and it is expected to redirect user to the /player/question when the displaySingleQuestion is called.
On the new route /player/question i then want to fetch the data from IndexDb and update the state of that component using the useEffect code below:
Code snippet Four
useEffect(()=>{
const getAllUserFromIndexDb = async()=>{
try{
const result = await new Promise((resolve, reject) => {
service.getStudents().then(res=>resolve(res)).catch(err=>reject(err))
});
console.log('it did not get to the point i was expecting',result)
if(result[0]){
console.log('it got to the point i was expecting')
const singleQuestion = result[0];
const questionPage = playerQuestionToDisplay;
questionPage.name = singleQuestion.name;
questionPage.anwserRatio = singleQuestion.anwserRatio;
questionPage.answer = singleQuestion.answer;
questionPage.options = singleQuestion.options;
questionPage.playerId = singleQuestion.playerId;
questionPage.question = singleQuestion.question;
questionPage.timePerQuestion = singleQuestion.timePerQuestion;
return setplayerQuestionToDisplay({playerQuestionToDisplay:questionPage})
}
}
catch(error){
console.log(error)
}
}
getAllUserFromIndexDb();
return function cleanup() {
setplayerQuestionToDisplay({playerQuestionToDisplay:{}})
}
},[history.location.pathname]);
The problem is that only one Button click (Code snippet three)(displaySingleQuestion()) triggers the whole functionality and redirect to the /player/question page but in this new route the state is not been set until a page reload as occurred, i tried debugging the problem and i found out that when the button is clicked i found out that Code snippet two is executed last hence when Code snippet Four ran it was in promise and until a page reloads occurs the state of the component is undefined
Thanks for reading, Please i would appreciate any help in resolving this issue.
I'm new in Jest and I'm trying to make test to my async action creators. The problem is that I only have seen examples of tests where there is a method for the action creator and other separate method for the dispatch. My actions creators have the dispatch as an async callback function, but I can't figure out how to make the test.
My Action Reducer looks like this:
export const postConnection = (connectionUrl, username, password) => {
return async (dispatch) => {
const response = await requireAuthActions.post('/db/conexion', JSON.stringify({ connectionUrl, username, password }));
dispatch({ type: POST_CONNECTION, payload: response.data });
history.push('/conexiones');
}
}
And I have trying to do the test script but when I run it it throws me an error. The script looks like this:
describe('Pruebas de Conexiones', () => {
describe('Action Creators', () => {
it("Crear conexión", async () => {
const dispatch = jest.fn();
const {data} = await postConnection('www.google.com', 'root', 'password')(dispatch);
dispatch({});
});
});
});
And when I try to run the test script it throws me the next error:
Request failed with status code 401
at createError (node_modules/axios/lib/core/createError.js:16:15)
at settle (node_modules/axios/lib/core/settle.js:17:12)
at IncomingMessage.handleStreamEnd (node_modules/axios/lib/adapters/http.js:237:11)
Is there a way of test actions creators like the mine?
EDIT
Now I have a mocks file with a mock for axios that looks like this:
const mockAxios = jest.genMockFromModule('axios');
mockAxios.create = jest.fn(() => mockAxios)
export default mockAxios
But now my problems is that I don't know how to implement this in the test file. For now I have something like this:
import thunk from 'redux-thunk';
import mockAxios from 'axios';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
describe('Test de conexiones', () =>{
describe('Action Creators', () =>{
it('Agregar conexión', async () =>{
const store = mockStore({});
const mockData = {data:123};
mockAxios.get.mockImplementationOnce(()=>Promise.resolve({data: mockData}));
const expectedActions = [
{type: POST_CONNECTION, data: mockData}
]
const dispatch = jest.fn();
const response = await postConnection('www.google.com','root','password')(dispatch);
});
});
});
And it throws me the next error:
TypeError: Cannot read property 'data' of undefined
12 | return async (dispatch) => {
13 | const response = await requireAuthActions.post('/db/conexion', JSON.stringify({ connectionUrl, username, password }));
> 14 | dispatch({ type: POST_CONNECTION, payload: response.data });
| ^
15 | history.push('/conexiones');
16 | }
17 | }
I see 2 approaches possible: using real store or redux-mock-store
Steps are similar:
Set up store.
mock external API like http call or at least requireAuthActions module with some response(successful or failed)(check jest's docs on mocking)
make a pause to await(await Promise.resolve();, flush-promises or setTimeout(..., 0)) all async things like Promise.resolve() you mocked in #2.
if you use real store - then check against store(with selectors if you use this level of abstraction) if it contains data you would expect to see based on response you mocked in #2
if it's redux-mock-store you cannot verify data in state(so reducers should be tested separately) so just validate list of actions generated - if they meet expectations based on response mocked in #2 or not.