Generic dataloader for redux-thunk using axios - javascript

I am developing an application where there are lots of async actions. I wanted to go with redux-saga but most have insisted to continue with redux-thunk. In redux-thunk, inside each action we have to work with async operation using then, dispatch, catch, etc. This makes looks actions so messy and lots of code will be repeated. I wanted to create a generic dataLoader for the use of redux-thunk and axios but could not consider for both post(might be token or not) and get option.
Here is my attempt:
export class Company {
/**
* Generic api data loader
*/
static dataLoader(apiUri, onSuccess, onError, data, ...actionArguments) {
const requestURL = `${API_BASE}${apiuri}`;
try {
let options;
if (data !== undefined) {
// if we have data to post
options = {
method: 'POST',
url: requestURL,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
};
}
}
return function(dispatch) {
axios(options)
.then(response => {
dispatch({
type: onSucess,
payload: response.data
});
})
.catch(error => {
dispatch({ type: onError, payload: err});
});
}
}
static get(apiUri, onSuccess, onError, ...actionArguments) {
return this.dataLoader(apiUri, onSuccess, onError, undefined, ...actionArguments);
}
/*
* Shorthand POST function
*/
static post(apiUri, onSuccess, onError, data, ...actionArguments) {
return this.dataLoader(apiUri, onSuccess, onError, data, ...actionArguments);
}
}
I want to convert the following code to further this one:
export function showResultofApartment() {
return (dispatch) => {
dispatch({ type: 'APARTMENT_FETCH_START' });
const token = localStorage.getItem('token');
return axios.get(`${API_URL}/newoffers/apartment/`)
.then((response) => {
console.log('response apart', response.data);
dispatch({ type: 'APARTMENT_FETCH_SUCCESS', payload: response.data });
})
.catch((err) => {
dispatch({ type: 'APARTMENT_FETCH_FAILURE', payload: err });
});
};
}
to such or more efficient than this:
export function showResultofApartment() {
return(dispatch) => {
dispatch({ type: APARTMENT_FETCH_START });
const token = localStorage.getItem('token');
return Company.get('/apartments', APARTMENT_FETCH_SUCCESS, APARTMENT_FETCH_ERROR);
// if post then Company.post('/apartment', APARTMENT_POST_SUCCESS, APARTMENT_POST_ERROR, data)
}
}
This way it is considering only post request(if data !== undefined). How should i handle for both get and post efficiently?

Okay, why don't you handle it like this:
Company.js
import { merge } from 'lodash';
import axios from 'axios';
function getHeaders() {
return {
'Content-Type': 'application/json'
};
}
export class Company {
static callAPI(endpoint, extendedOptions, onSuccess, onError) {
const initalHttpData = {
method: 'GET', // By default it's GET in case you didnt specify anything
headers: getHeaders(),
url: `${API_BASE}${endpoint}`
};
// merge takes care of replacing or adding the specific key's provided via the extendedOptions
const options = merge(initalHttpData, extendedOptions);
// Fire the request for the prepared options.
let request = axios(options);
// The request once fired, needs it's success handler and error handler.
return function(dispatch) {
request
.then(response => {
dispatch({
type: onSucess,
payload: response.data
});
})
.catch(error => {
dispatch({ type: onError, payload: err});
});
}
};
}
Then we can use actions to specifically pass things to this api util:
GET API call:
// GET Action
export function showResultofApartment() {
return (dispatch) => {
dispatch({ type: APARTMENT_FETCH_START });
const token = localStorage.getItem('token');
// FOR GET API
return Company.callApi('/apartments', {}, APARTMENT_FETCH_SUCCESS, APARTMENT_FETCH_ERROR);
}
}
POST API call:
// POST Action
export function showResultOfAppartmentPost() {
return (dispatch) => {
dispatch({ type: APARTMENT_FETCH_START });
const token = localStorage.getItem('token');
// This will merge, essentially replace the method=GET once it gets called.
const extendedOptions = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
}
// FOR GET API
return Company.callApi('/apartments', extendedOptions, APARTMENT_FETCH_SUCCESS, APARTMENT_FETCH_ERROR);
}
Thus, giving the action, to define it's own set of API body or requests.

Related

React and Redux: 401 Unauthorized Error POST API Request

//Postman //React App
For my react app, when dispatching apiPostRequest() I am getting 401 unauthorized error when it hits API POST Request Middleware even though the user is authenticated and token is stored in local storage. The confusing part is that my GET and DELETE api requests work just fine. So I cannot figure out why then is POST api returning 401 unauthorized error? It works fine in Postman. Any help will be greatly appreciated.
//Accept request action
export const acceptRequest = (requestId) => ({
type : ACCEPT_REQUEST,
payload: requestId
});
// Accept request middleware
export const acceptRequestFlow = ({dispatch, getState}) => next => action => {
next(action);
if(action.type === ACCEPT_REQUEST){
const acceptRequestId = action.payload
console.log(acceptRequestId)
const URL = `/api/userConnections/acceptRequest/${action.payload}`;
dispatch(apiPostRequest(URL, setAuthorizationHeader(getState), acceptRequestId, ACCEPT_REQUEST_SUCCESS, ACCEPT_REQUEST_ERROR));
}
};
//API POST, DELETE, GET request actions
export const apiPostRequest = (url, config, body, onSuccess, onError) => ({
type: POST_API_REQUEST,
meta: {url, config, body, onSuccess, onError}
});
export const apiDeleteRequest = (url, config, id, onSuccess, onError) => ({
type: DELETE_API_REQUEST,
meta: {url, config, id, onSuccess, onError}
});
export const apiGetRequest = (url, config, onSuccess, onError) => ({
type: GET_API_REQUEST,
meta: {url, config, onSuccess, onError}
});
//API POST, DELETE, GET request middleware
export const api = ({dispatch}) => next => action => {
if(action.type === POST_API_REQUEST) {
const {url, config, body, onSuccess, onError} = action.meta;
axios.post(url, config)
.then((data) => {dispatch({ type: onSuccess, payload: body})})
.catch(error => dispatch({ type: onError, payload: error }))
}
if(action.type === GET_API_REQUEST) {
const {url, config, onSuccess, onError } = action.meta;
axios.get(url, config)
.then((data) => {dispatch({ type: onSuccess, payload: data})})
.catch(error => dispatch({ type: onError, payload: error }))
}
if(action.type === DELETE_API_REQUEST) {
const {url, config, id, onSuccess, onError } = action.meta;
axios.delete(url, config)
.then((data) => { dispatch({ type: onSuccess, payload: id })})
.catch(error => dispatch({ type: onError, payload: error }))
}
return next(action)
};
//Setting Auth Header
export const setAuthorizationHeader = (getState) =>{
const token = getState().auth.token;
const config = {
headers:{
"Content-Type": "application/json"
}
}
if(token){
config.headers['Authorization'] = token;
}
return config;
};
The issue is that you are using the post method incorrectly.
As you can see from the image taken from axios doc, when making a post without specifying the parameter name, then the second parameter passed in is perceived as data.
So try: axios.post(url, {}, config)
try this:
//Setting Auth Header
export const setAuthorizationHeader = (getState) =>{
const token = getState().auth.token;
const config = {
headers:{
"Content-Type": "application/json"
}
}
if(token){
// change 'x-authorization' to 'Authorization'
config.headers['Authorization'] = token;
}
return config;
};

getting requested files as an empty array in Laravel API from javascript

here is my javascript form handler
where i get data from the form to send it as request to API
import { Store } from './http/requests.js';
$(document).ready(function () {
$('#form_submit').submit(function (e) {
e.preventDefault();
var formData = new FormData(this);
Store(formData);
});
});
js requests file handler
where i use customized post,get functions to send data with options that i provide on it
import { get, post } from '../helper.js';
let pageName = window.location.pathname;
pageName = pageName.slice(1, pageName.length - 5);
export const Store = (value) => {
switch (pageName) {
case 'add_car':
post('user/create_car', value, true, 'multipart/form-data')
.then((res) => {
console.log(res);
return res;
})
.catch((err) => console.log(err));
default:
break;
}
};
then the helper file where i use fetch get,post with option that i receive from "requests.js" file and provide it here
import { Local as loc } from './localStorage.js';
const API_URL = 'http://127.0.0.1:8000/api';
// token if exists in localStorage
const token = loc('get', 'token');
// POST Request
export const post = (
url,
formData,
auth = false,
type = 'application/json',
providedToken = token,
) => {
return fetch(`${API_URL}/${url}`, {
method: 'POST',
body: JSON.stringify(formData),
headers: {
'Content-Type': type,
Authorization: auth ? `Bearer ${providedToken}` : null,
},
})
.then((res) => res.json())
.then((res) => {
console.log(res);
return res;
})
.catch((err) => console.log(err));
};
and finally the Laravel API Cotroller where i tried to debug the issue
public function create_car(Request $request)
{
return (response()->json([
"files" => $_FILES,
"all Request data" => $request,
]));
}
the response i get when i send data from javascript to Laravel API
API gives me back this empty object as a response
it's seems like fetch has a problem ... anyway i just replaced fetch library with axios and everything runs perfectly
here is what i did on helper.js file
// POST Request
export const post = (
url,
formData,
auth = false,
type = 'application/json',
providedToken = token,
) => {
return axios({
method: 'POST',
url: `${API_URL}/${url}`,
data: formData,
headers: {
'Content-Type': type,
Authorization: auth ? `Bearer ${providedToken}` : null,
},
})
.then((res) => {
console.log(res);
return res.data;
})
.catch((err) => console.log(err.data));
};

Do I need redux-promise in this case?

So I am testing this api request inside my react-redux application:
import $ from 'jquery';
window.$ = $;
const API_KEY = '<api-key>';
const ROOT_URL = `https://api.behance.net/v2/users?client_id=${API_KEY}`;
export const FETCH_USER = 'FETCH_USER';
export function fetchUser(users) {
const request = $.ajax({
url: `${ROOT_URL}&q=${users}`,
type: 'get',
data: { users: {} },
dataType: 'jsonp'
})
.done(response => {
console.log('Request:', request);
})
.fail(error => {
console.log('Ajax request fails');
console.log(error);
});
return {
type: FETCH_USER,
payload: request
};
}
However, in the Chrome console for Request: I am getting an object with readyState instead of a Promise, do I even need to have the package redux-promise at this point?
I see what you are trying to do, but I don't think is a good idea to send a promise to a reducer instead I will recommend you to use the middleware redux-thunk.
I will rewrite your action this way
export const fetchUser= ()=>(dispatch,getState)=>{
let params={
method:'get',
body:{ users: {} },
}
fetch( `${ROOT_URL}&q=${users}`, params).then
.then(response => response.json())
.then((response) =>{
dispatch({
type:Const.ON_RESPONSE_OK,
payload:response
})
})
.catch(error => {
dispatch({
type:Const.ON_RESPONSE_ERROR,
payload:Error
})
});
}
and rewrite the reducer to handle the payloads

React and Node Login implementation

I have an application in which i need to make auths
i made the exact replica of react-routers auth workflow(https://reacttraining.com/react-router/web/example/auth-workflow)
Auth.js
const auth = {
isAuthenticated: false,
authenticate(data, scb, fcb) {
fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(res => {
console.log(res);
localStorage.setItem("token", res.token);
this.isAuthenticated = true;
scb();
})
.catch(ex => {
this.isAuthenticated = false;
fcb();
});
},
signout(cb) {
localStorage.removeItem("token");
this.isAuthenticated = false;
cb();
},
checkSignIn(cb, fcb) {
fetch("api/user/me", {
headers: {
"content-type": "application/json",
"x-auth-token": localStorage.token
}
})
.then(res => res.json())
.then(res => {
console.log("here");
this.isAuthenticated = true;
cb();
})
.catch(err => {
console.log("here in checksign in catch");
this.isAuthenticated = false;
fcb();
});
}
};
export default auth;
Now while when the page loads i use checkSignIn method and redirect the user to dashboard if the user is authenticated
The main problem is if the token expires , my API call will be rejected , now i want to redirect the user back to Login , I use the signout method and pass the callback to change the state of the component and return but the problem is I am seeing too much repetition in the code , at every api call i have to pass a callback to change the state of the page if the api call fails or i have to use withRouter (HOC) but i have to wrap my every component in that.
Is there any alternative way to implement this functionality

React-Redux How to call new dispatch based on a success condition

I have banged around in this for a while now. Looked at this question but its not quite what I want.
In a nutshell... I have const expression = to a function that is chained to another function that makes an API call in a separate file to LoginContainer but in the same folder -(its called reducer.js but has the actions as well at this stage). If successful it receives a token which it saves in local storage. this works fine.
Here it is.
import { fetch, addTask } from 'domain-task'
import { saveJwt, clearJwt } from '../auth/jwt'
import { handleErrors } from '../utils/http'
const REQUEST_LOGIN_TOKEN = 'REQUEST_LOGIN_TOKEN'
const RECEIVE_LOGIN_TOKEN = 'RECEIVE_LOGIN_TOKEN'
const ERROR_LOGIN_TOKEN = 'ERROR_LOGIN_TOKEN'
const REQUEST_USER = 'REQUEST_USER'
const RECEIVE_USER = 'RECEIVE_USER'
const ERROR_USER = 'ERROR_USER'
// ******************* action
export const requestLoginToken = (username, password) =>
(dispatch, getState) => {
dispatch({ type: REQUEST_LOGIN_TOKEN, payload: username })
const payload = {
userName: username,
password: password,
}
const task = fetch('/api/jwt', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json;charset=UTF-8'
},
})
.then(handleErrors)
.then(response => response.json())
.then(data => {
dispatch({ type: RECEIVE_LOGIN_TOKEN, payload: data })
saveJwt(data)
})
.catch(error => {
clearJwt()
dispatch({ type: ERROR_LOGIN_TOKEN, payload: error.message })
})
addTask(task)
return task
}
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import Login from './Login'
import { requestLoginToken } from './reducer'
class LoginContainer extends Component {
static contextTypes = {
router: PropTypes.object.isRequired
}
componentWillReceiveProps(nextProps) {
if (nextProps.isAuthorised) {
this.context.router.push('/')
}
}
submit = (values) => {
console.log('got values!', values)
this.props.requestLoginToken(values.username, values.password)
}
render() {
return (
<Login onSubmit={this.submit} />
)
}
}
const mapStateToProps = (state) => ({
isAuthorised: state.login.isAuthorised,
})
const mapDispatchToProps = (dispatch) => ({
requestLoginToken: (username, password) => dispatch(requestLoginToken(username, password)),
//requestSelectData: (values = {}) => dispatch(requestSelectData(values = {})),
})
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer)
In the loginContainer (above), Once the "userName" and "password" have been entered and the submit button clicked, the expression "requestLoginToken" is called.
My Problem
I want to fetch a significant amount of data based on the above expression "requestLoginToken" successfully saving a JWT token into local storage. It does this successfully now with the right username and password.
I know I can't make another call from within the expression "requestLoginToken" using a ".then" as it specifically needs to retrieve and then save a token first - I have to wait till it finishes to know if I have a token. I need to run a second expression that only gets run if this promise is successful ie via a conditional statement. "If (JWT) etc"
1) Could someone tell me where and how I add this conditional statement. Im thinking its in the Logincontainer in the submit? ..how do would I structure the condition?
2) Where and how do I add the const = function for the retrieval of the data eg if I place it in another separate file do I still or even need to register it in mapDispatchToProps in the loginContainer etc
EDIT
Taking Nate Kimball's answer and running with it. Decided to split it out into its own "const" called "selectData" which I plan to call right underneath the line "saveJwt(data)".
However I find I am now getting an error:
Unexpected Token , expected
Its on the very last line of the following code block below.. (right curly bracket has red under it) checked it for sytax but cant workout why.
I think the approach is correct though.
const selectData = () => {
dispatch({ type: REQUEST_SELECT_DATA })
const token = jwt.access_token
const headers = new Headers({
'Authorization': `Bearer ${token}`
})
const selectData = fetch('/api/SelectData/SelectData', {
method: 'GET',
headers,
})
.then(handleErrors)
.then(response => response.json())
.then(data => {
dispatch({ type: RECEIVE_SELECT_DATA, payload: data })
.catch(error => {
clearJwt()
dispatch({ type: ERROR_SELECT_DATA, payload: error.message })
})
}
}
I don't see any reason why you couldn't nest a second fetch from within your action after a successful call:
export const requestLoginToken = (username, password) =>
(dispatch, getState) => {
dispatch({ type: REQUEST_LOGIN_TOKEN, payload: username })
const payload = {
userName: username,
password: password,
}
const task = fetch('/api/jwt', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json;charset=UTF-8'
},
})
.then(handleErrors)
.then(response => response.json())
.then(data => {
dispatch({ type: RECEIVE_LOGIN_TOKEN, payload: data })
saveJwt(data)
// Since this is where you receive your login token,
// You can dispatch an action to acknowledge you're fetching:
dispatch({ type: SECOND_DATA_FETCHING })
// This is also where you would make your next call:
fetch('/api/etc', { ...config })
.then(response => {
// You can use your reducer to both inform a successful call &
// store the received data
dispatch({ type: SECOND_DATA_SUCCESS, payload: response.data })
})
.catch(error => {
// Let your app know the call was unsuccessful:
dispatch({ type: SECOND_DATA_FAILED, payload: error.message })
})
// Note: if you don't like the nested ugliness, you could optionally
// put this entire nested fetch chain into a separate action and just
// dispatch that when you get your token.
})
.catch(error => {
clearJwt()
dispatch({ type: ERROR_LOGIN_TOKEN, payload: error.message })
})
addTask(task)
return task
}
At that point, all you need to do is update your mapStateToProps function in your component to receive the data and/or the status of that second layer of fetched data:
// Make sure you have a default status for that second data
// just in case your token call fails.
const mapStateToProps = (state) => ({
isAuthorised: state.login.isAuthorised,
secondData: state.login.secondData,
secondDataStatus: state.login.secondDataStatus
})
You can use requestLoginToken in another action creator:
function loginAndFetch() {
return function(dispatch, getState) {
dispatch(requestLoginToken()).then(token => {
return fetch(...) // use token here
})
}
}
As an alternative, you could save the token you got to the store, then have another component listen to changes to the token and dispatch another action when the token changes.
class Container extends Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.token != prevProps.token) {
dispatch(fetchSignificantAmountOfData())
}
}
}
Container is connected and maps the stored token into props.token
You can write a custom middleware to solve this problem like this:
https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/redux/middleware/clientMiddleware.js
And then, you can use the action like this:
export function myAction() {
return {
types: [LOAD, SUCESS, FAIL],
promise: (client) => client.get('/some_api')
};
}
The middleware will dispatch the LOAD reducer first, then if promise is resolve, it call SUCESS; Otherwise, FAIL is called.

Categories