I'm trying to get the value from a simple ajax request, but I don't understand how to do that. Here is the code:
Rx.Observable
.ajax({ url: 'https://jsonplaceholder.typicode.com/posts', method: 'GET', responseType: 'json' })
.subscribe(function(data) { return data.response; });
I searched everywhere and there is no simple explanation.
Thanks!
Observable.ajax can accept string or Object with the following interface:
interface AjaxRequest {
url?: string; // URL of the request
body?: any; // The body of the request
user?: string;
async?: boolean; // Whether the request is async
method?: string; // Method of the request, such as GET, POST, PUT, PATCH, DELETE
headers?: Object; // Optional headers
timeout?: number;
password?: string;
hasContent?: boolean;
crossDomain?: boolean; //true if a cross domain request, else false
withCredentials?: boolean;
createXHR?: () => XMLHttpRequest; //a function to override if you need to use an alternate XMLHttpRequest implementation
progressSubscriber?: Subscriber<any>;
responseType?: string;
}
see AjaxObservable.ts on GitHub
And here is examples:
const { Observable, combineLatest } = rxjs; // = require("rxjs")
const { ajax } = rxjs.ajax; // = require("rxjs/ajax")
const { map } = rxjs.operators; // = require("rxjs/operators")
// simple GET request example
const simple$ = ajax('https://httpbin.org/get');
// POST request example
const complex$ = ajax({
url: 'https://httpbin.org/post',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-rxjs-is': 'Awesome!'
},
body: {
hello: 'World!',
}
});
const htmlSubscription = combineLatest(simple$, complex$)
.subscribe(([simple, complex]) => {
const simpleResponse = JSON.stringify(simple.response, null, 2);
const complexResponse = JSON.stringify(complex.response, null, 2);
document.getElementById('root').innerHTML = `
<div>
<span><b>GET</b> https://httpbin.org/get</span>
<pre>${simpleResponse}</pre>
<span><b>POST</b> https://httpbin.org/post</span>
<pre>${complexResponse}</pre>
</div>`;
});
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
<div id="root">loading ...</div>
Typescript version
"dependencies": {
"rxjs": "^5.1.0"
}
and
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/dom/ajax';
import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch';
const posts$ = Observable
.ajax('https://jsonplaceholder.typicode.com/posts')
.map(e => e.response);
const htmlSubscription = posts$
.subscribe(res => {
console.log(JSON.stringify(res, null, 2));
});
You want to use switchMap ..
const response$ = request$.switchMap((url) => {
console.log(url);
return fetch(url).then(res => res.json());
});
switchMap flattens a stream of streams and converts it to a stream that just emits the inner streams responses. If a second innerStream is emitted, the first stream is killed and the second one proceeds on its own.
See this bin which demos streamed requests over HTTP .. https://jsbin.com/duvetu/32/edit?html,js,console,output
Related
After trying for hours to try implementing a Hmac token on a react Apollo client request, I just couldn't find a way to retrieve the final request body that is being sent.
Is there a way to get the request body sent to the server before the client sends it or no way at all ? Not being able to get body hash causes for some security concerns for Mitm attacks since using a jwt the way Apollo docs show it doesn't provide with a way of signing the content.
Based on this, I came up with something
import React from "react";
import { render } from "react-dom";
import {
ApolloClient,
ApolloProvider,
HttpLink,
ApolloLink,
InMemoryCache,
concat,
useQuery,
gql,
} from "#apollo/client";
import CryptoJS from "crypto-js";
import { print } from "graphql/language/printer";
const httpLink = new HttpLink({ uri: "http://localhost:3000/dev/query" });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const { query, variables, operationName } = operation,
payload = {
variables,
operationName,
query: print(query),
};
console.log(payload);
// custom headers
const token = getAuthHeader(payload, "key"), // create HMAC here
headers = {
hmac: token,
};
operation.setContext({
headers: {
...headers,
},
});
return forward(operation);
});
const client = new ApolloClient({
cache: new InMemoryCache({
addTypename: false, //quite important
}),
link: concat(authMiddleware, httpLink),
});
function getAuthHeader(payload_string, sk) {
var str = JSON.stringify(payload_string);
console.log(str);
var hmac = CryptoJS.HmacSHA256(str, sk).toString(CryptoJS.enc.Hex);
return hmac;
}
function MakeRequest() {
const request = `
query {
items {
_id
}
}
`;
const { loading, error, data } = useQuery(gql(request));
//Whatever you wanna do with it.
}
function App() {
return (
<div>
<MakeRequest />
</div>
);
}
render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
);
Seems to work correctly.
I have an input field that contains a postcode. On submit I want to pass the postcode as an object to an axios request. I have created a CodeSandbox here: https://codesandbox.io/s/determined-beaver-8ebqc
The relevant code is:
App.vue
<template>
<div id="app">
<input v-model="postcode" type="text" placeholder="Enter Postcode">
<button #click="getAddress">Submit</button>
</div>
</template>
<script>
import useAxios from "#/composition/use-axios";
export default {
name: "App",
setup() {
const { sentData, response, fetchData } = useAxios(
"api/address/lookup-address",
"postcode",
"Failed Finding Address"
);
return {
postcode: sentData,
address: response,
getAddress: fetchData
};
}
};
</script>
use-axios.js
import { reactive, toRefs } from "#vue/composition-api";
import axios from "axios";
export default function (url, objectData, errorMessage) {
const state = reactive({
sentData: null,
response: null
});
const fetchData = async () => {
console.log("Sent Data:", state.sentData);
console.log("Response:", state.response);
console.log("URL:", url);
console.log("Object Data:", objectData);
console.log("Object:", { [objectData]: state.sentData });
console.log("Error Message:", errorMessage);
const config = { headers: { "Content-Type": "application/json" } };
try {
const res = await axios.post(url, [objectData]: state.sentData, config);
state.response = await res.data;
} catch (error) {
// Error handling stuff
}
}
return { ...toRefs(state), fetchData };
}
Converting the postcode input string to an object in this way seems very hacky. Also, this would get very messy if I needed to send multiple parameters to the axios request. Say if I want to pass { id: "1234", user: "Me" }, I would like to be able to construct that like:
sentData = { id: ${id}, user: ${user} }
But I'm not able to do this. What is the proper way to do this so that I can keep use-axios generic?
You will need to import ref, reactive and computed from the composition-api and then use them like this:
<script>
import useAxios from "#/composition/use-axios";
import { ref, reactive, computed } from "#vue/composition-api";
export default {
name: "App",
setup() {
let object = ref("");
let state = reactive({ postcode: "" });
const sentDataObject = computed(() => {
state.postcode = object;
return state;
});
const addressList = useAxios(
"api/address/lookup-address",
sentDataObject.value,
"Failed Finding Address"
);
return {
addresses: addressList.response,
postcode: object,
getAddress: addressList.fetchData
};
}
};
</script>
change use-axios.js to:
import { reactive, toRefs } from "#vue/composition-api";
import axios from "axios";
export default function (url, objectData, errorMessage) {
const state = reactive({
sentData: null,
response: null
});
const fetchData = async () => {
console.log("Sent Data:", state.sentData);
console.log("Response:", state.response);
console.log("URL:", url);
console.log("Object Data:", objectData);
console.log("Error Message:", errorMessage);
const config = { headers: { "Content-Type":
"application/json" } };
try {
const res = await axios.post(url, objectData, config);
state.response = await res.data;
} catch (error) {
// Error handling stuff
}
};
return { ...toRefs(state), fetchData };
}
See Codesandbox demo here: https://codesandbox.io/s/dawn-glade-ewzb7
I have created this function because for all the requests my application sends out using http.post, this is how different parts handle the response. So rather than duplicating the code, I thought to create a function. But I am unable to figure out how to unit test this function.
private editAnswerSubject: Subject<Result>;
subscribeToReturnedObservable(observable:Observable<any>, subject:Subject<Result>) {
observable.subscribe((res) => {
const ev = <HttpEvent<any>>(res);
if (ev.type === HttpEventType.Response) {
const isResponseStructureOK: boolean = this.helper.validateServerResponseStructure(ev.body);
if (isResponseStructureOK) {
const response: ServerResponseAPI = ev.body;
subject.next(new Result(response.result, response['additional-info']));
} else {
subject.next(new Result(messages.error, messages.invalidStructureOfResponse));
}
}
},
(error: ServerResponseAPI) => {
const errorMessage: string = this.helper.userFriendlyErrorMessage(error);
subject.next(new Result(messages.error, errorMessage));
},
() => { // observable complete
});
}
editAnswer(answer: Answer): any {
const observable = this.bs.editAnswer(answer)
this.subscribeToReturnedObservable(observable,this.editAnswerSubject);
}
The test I have written so far is
describe('subscribeToReturnedObservable tests:', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [QuestionManagementService, HelperService, WebToBackendInterfaceService, AuthService, HttpClient, HttpHandler]
});
});
fit('should call send next value for the subject is the response from the server is ok', () => {
const questionService:QuestionManagementService = TestBed.get(QuestionManagementService);
const body = {"result":"success", "additional-info":"some additional info"};
const receivedHttpEvent = new HttpResponse({body:body});
let observable = new Observable();
spyOn(observable,'subscribe').and.returnValue(receivedHttpEvent);
spyOn(questionService['editQuestionSubject'],'next');
questionService.subscribeToReturnedObservable(observable,questionService['editQuestionSubject']);
observable.subscribe();
expect(questionService['editQuestionSubject'].next).toHaveBeenCalled();
});
});
But it get error Expected spy next to have been called.
I did this (hoping that it is the right way). The scope of testing is to check that the Subject's next is called correctly. So create an Observable using of and let the code flow from there.
fit('should call send next value for the subject is the response from the server is ok', () => {
const questionService:QuestionManagementService = TestBed.get(QuestionManagementService);
const helperService:HelperService = TestBed.get(HelperService);
const body = {"result":"success", "additional-info":"some additional info"};
const receivedHttpEvent = new HttpResponse({body:body});
const expectedResult = new Result('success', 'some additional info');
spyOn(helperService,'validateServerResponseStructure').and.returnValue(true);
let observable = of(receivedHttpEvent);
spyOn(questionService['editQuestionSubject'],'next');
questionService.subscribeToReturnedObservable(observable,questionService['editQuestionSubject']);
expect(questionService['editQuestionSubject'].next).toHaveBeenCalledWith(expectedResult);
});
I just get to experiment with Redux and I know that middleware is essential to make ajax calls. I've installed redux-thunk and axios package separately and tried to hook my result as a state and render the ajax result to my component. However my browser console displays an error and my reducer couldn't grab the payload.
The error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
This is part of my code and how the middleware is hooked up:
//after imports
const logger = createLogger({
level: 'info',
collapsed: true,
});
const router = routerMiddleware(hashHistory);
const enhancer = compose(
applyMiddleware(thunk, router, logger),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
// store config here...
my action:
import axios from 'axios';
export const SAVE_SETTINGS = 'SAVE_SETTINGS';
const url = 'https://hidden.map.geturl/?with=params';
const request = axios.get(url);
export function saveSettings(form = {inputFrom: null, inputTo: null}) {
return (dispatch) => {
dispatch(request
.then((response) => {
const alternatives = response.data.alternatives;
var routes = [];
for (const alt of alternatives) {
const routeName = alt.response.routeName;
const r = alt.response.results;
var totalTime = 0;
var totalDistance = 0;
var hasToll = false;
// I have some logic to loop through r and reduce to 3 variables
routes.push({
totalTime: totalTime / 60,
totalDistance: totalDistance / 1000,
hasToll: hasToll
});
}
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
})
);
}
}
reducer:
import { SAVE_SETTINGS } from '../actions/configure';
const initialState = { form: {configured: false, inputFrom: null, inputTo: null}, routes: [] };
export default function configure(state = initialState, action) {
switch (action.type) {
case SAVE_SETTINGS:
return state;
default:
return state;
}
}
you can see the state routes has size of 0 but the action payload has array of 3.
Really appreciate any help, thanks.
It looks like you have an unnecessary dispatch in your action, and your request doesn't look to be instantiated in the correct place. I believe your action should be:
export function saveSettings(form = { inputFrom: null, inputTo: null }) {
return (dispatch) => {
axios.get(url).then((response) => {
...
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
});
};
}
For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.
Action
import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';
export default function getBook() {
return {
type: GET_BOOK,
data: $.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return data;
})
};
}
Reducer
import {GET_BOOK} from '../actions/books';
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
return state;
default:
return state;
}
};
export default booksReducer;
Container
How display data in container?
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';
function mapStateToProps(state) {
return {
books: state.data.books,
};
}
function mapDispatchToProps(dispatch) {
return {
getBooks: () => dispatch(getBook()),
};
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
books: PropTypes.array.isRequired,
};
render() {
const {books} = this.props;
return (
<div>
<Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
<ul>
{books.map((book, index) =>
<li key={index}>
<Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
"{book.name}"</div></MUIButton></Link>
<Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
{book.author}</div></MUIButton></Link>
</li>
)}
</ul>
</div>
);
}
}
export default booksPage;
Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.
Installation & usage: Redux-thunk
export function fetchBook(id) {
return dispatch => {
dispatch(setLoadingBookState()); // Show a loading spinner
fetch(`/book/${id}`, (response) => {
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setBook(response.json)); // Use a normal function to set the received state
}else {
dispatch(someError)
}
})
}
}
function setBook(data) {
return { type: 'SET_BOOK', data: data };
}
You should use Async Actions described in Redux Documentation
Here an example of reducer for async action.
const booksReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_BOOK':
return action.data;
default:
return state;
}
};
export default booksReducer;
and then you create your Async Action.
export const getBook() {
return fetch('/api/data')
.then(response => response.json())
.then(json => dispatch(resolvedGetBook(json)))
}
export const resolvedGetBook(data) {
return {
type: 'RESOLVED_GET_BOOK',
data: data
}
}
Several Notes:
We could return Promise (instead of Object) in action by using redux-thunk middleware.
Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
Read the complete guide from redux docs.
You should be able to use dispatch inside the callback (if you pass it as an argument):
export default function getBook(dispatch) {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return dispatch({type:'GET_BOOK', data: data});
});
}
Then, pass dispatch to the action:
function mapDispatchToProps(dispatch) {
return {
getBooks: () => getBook(dispatch),
};
}
Now, you should have access to the action.data property in the reducer:
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
//action.data <--- here
return state;
default:
return state;
}
};
You might want to separate concerns, to keep action creators "pure".
Solution; write some middleware. Take this for example (using superagent).
import Request from 'superagent';
const successHandler = (store,action,data) => {
const options = action.agent;
const dispatchObject = {};
dispatchObject.type = action.type + '_SUCCESS';
dispatchObject[options.resourceName || 'data'] = data;
store.dispatch(dispatchObject);
};
const errorHandler = (store,action,err) => {
store.dispatch({
type: action.type + '_ERROR',
error: err
});
};
const request = (store,action) => {
const options = action.agent;
const { user } = store.getState().auth;
let method = Request[options.method];
method = method.call(undefined, options.url)
if (user && user.get('token')) {
// This example uses jwt token
method = method.set('Authorization', 'Bearer ' + user.get('token'));
}
method.send(options.params)
.end( (err,response) => {
if (err) {
return errorHandler(store,action,err);
}
successHandler(store,action,response.body);
});
};
export const reduxAgentMiddleware = store => next => action => {
const { agent } = action;
if (agent) {
request(store, action);
}
return next(action);
};
Put all this in a module.
Now, you might have an action creator called 'auth':
export const auth = (username,password) => {
return {
type: 'AUTHENTICATE',
agent: {
url: '/auth',
method: 'post',
resourceName: 'user',
params: {
username,
password
}
}
};
};
The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.
Your reducer handles all this, after you define the hooks:
import { Record } from 'immutable';
const initialState = Record({
user: null,
error: null
})();
export default function auth(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATE':
return state;
case 'AUTHENTICATE_SUCCESS':
return state.merge({ user: action.user, error: null });
case 'AUTHENTICATE_ERROR':
return state.merge({ user: null, error: action.error });
default:
return state;
}
};
Now inject all this into your view logic. I'm using react as an example.
import React from 'react';
import ReactDOM from 'react-dom';
/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';
// thunk is needed for returning functions instead
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';
// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';
// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';
/* vanilla index component */
import _Index from './components';
/* Redux reducers */
import reducers from './reducers';
/* Redux actions*/
import actionCreators from './actions/auth';
/* create store */
const store = createStore(
reducers,
applyMiddleware(
thunkMiddleware,
myNetMiddleware
)
);
/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {
const { auth } = state;
return {
auth
};
}, (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
})(_Index);
const provider = (
<Provider store={store}>
<Index />
</Provider>
);
const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);
All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.
Consider using the new thunk API
export const load = createAsyncThunk(
'example/api',
async (arg, thunkApi) => {
const response = await fetch('http://example.api.com/api')
if (response.status === 200) {
const json = await response.json()
return json
},
)
Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.
See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks