I'm new to react / redux
When I doing the project in jquery
I will make some functions like this:
errorHandle (code) {
if(code = 1){
$('.popUpA').show()
}
...
}
callAPI (){
//Do AJAX call
//If Error , call errorHandle()
}
In the new project,
I use the axios to call API in the api helper
export function getDataList(){
//axios....
}
export function getData(){
//axios....
}
And I use the Store to trigger the show/hide popUp , I will use dispatch(showPopup()) and dispatch(showPopup(hide)) in component
But I want that if api function have error , will pass the response in to the errorHandler , then dispatch the showPopup. I don't have idea how to add this into the exported function.
Any suggestion or example?
this is my abstraction of axios request, I use it in services that are used with Redux:
import axios from 'axios';
import { API } from '../../constants';
import { revokeAuthAction } from ;
export const getAuth = () => {
// Auth logic
};
/**
* Create an Axios Client with defaults
*/
const client = axios.create({
baseURL: API.BASEURL,
headers: {
Authorization: getAuth(),
'Access-Control-Max-Age': 1728000,
// 'X-Authorization-JWT':
},
});
/**
* Request Wrapper with default success/error actions
*/
const request = (options) => {
const onSuccess = (response) => options.raw ? response : response.data;
// console.debug('Request Successful!', response);
// If options.raw is true, return all response
const onError = (error) => {
// console.error('Request Failed:', error.config);
if (error.response) {
if (error.response.status === 401) {
// console.error('Unauthorized');
store.dispatch(revokeAuthAction());
} else {
// Request was made but server responded with something
// other than 2xx
// console.error('Status:', error.response.status);
// console.error('Data:', error.response.data);
// console.error('Headers:', error.response.headers);
}
} else {
// Something else happened while setting up the request
// triggered the error
// console.error('Error Message:', error.message);
}
return Promise.reject(error.response || error.message);
};
return client(options)
.then(onSuccess)
.catch(onError); // in realtà non catcho un bel niente perchè ritorno Promise.reject e quindi il giro ricomincia
};
export default request;
There are more library to handle redux async action calls. I use redux-thunk another well known library is redux-saga. With redux thunk you will add a middleware to redux and this way you can create async action creators which return a function, and they can call other action creators depending of the result of the async call.
You add the middleware this way:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
And your action creator will be something like this:
export function requestDataList() {
return function (dispatch: Function, getState: Function) {
return getDataList().then(resp => {
dispatch(dataListReceived(resp.data));
}).catch(error => {
dispatch(showPopup(title, error.message));
});
};
}
So if your getDataList retrun an axios promise, on success it will call an action wit the result. On error it can call the error dialog.
Related
I can't understand why I'm getting this error. I am trying to use the Vuex store in a composition function but it keeps throwing me this error about inject (I'm not even using inject). My app makes an await api call to the backend and if there is an error calls my composition function.
[Vue warn]: inject() can only be used inside setup() or functional components.
inject # runtime-dom.esm-bundler-9db29fbd.js:6611
useStore # vuex.esm-bundler.js:13
useErrorHandling # useErrorHandling.js:5
checkUserExists # auth.js:53
Here is my composition function
import { useStore } from 'vuex'
function useErrorHandling()
{
const store = useStore() // <-- this line
function showError(errorMessage) {
console.log(errorMessage)
}
return { showError }
}
export default useErrorHandling
If I remove this line then it doesn't throw that error
// const store = useStore() // <-- this line
UPDATE: this is how the function is called.
/**
* Check if a user exists in database
*/
static async checkUserExists(data)
{
const { env } = useEnv()
const { jsonHeaders } = useHTTP()
const { showError } = useErrorHandling()
try {
let response = await fetch(`${env('VITE_SERVER_URL')}/auth/check-user-exists`, {
method: 'POST',
body: JSON.stringify(data),
headers: jsonHeaders,
})
if (!response.ok) {
let errorMessage = {
statusText: response.statusText,
statusCode: response.status,
body: '',
url: response.url,
clientAPI: 'api/auth.js # checkUserExists',
}
const text = await response.text()
errorMessage.body = text
showError(errorMessage) // <-- here
return
}
response = await response.json()
return response.user_exists
} catch (error) {
alert('Error occured!')
console.log(error)
}
}
The error is telling you that useStore is only for use inside of components, since the module isn't a component. From the docs:
To access the store within the setup hook, you can call the useStore function. This is the equivalent of retrieving this.$store within a component using the Option API.
To use the store in a module, you can import { store } from the module where it was created:
store.js
export const store = createStore({
...
})
Other module
import { store } from './store'
I am using Vuex/Axios to make GET requests to an API. When a component mounts, I am dispatching an action to the Vuex store and making the Axios GET request. In the Vuex action, the Axios GET request returns the response as expected but the response inside the component is returning undefined. What am I doing wrong?
axios/index.js
import axios from 'axios';
const API_URL = 'http://localhost:3000/api/v1/';
const plainAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
export { plainAxiosInstance };
Vuex module: store/modules/character.js. In this file response logs the correct response. The fetchCharacters event gets triggered in a component.
import { plainAxiosInstance } from '#/axios';
const characterModule = {
namespaced: true,
state: {
characters: []
},
mutations: {
SET_CHARACTERS(state, characters) {
state.characters = characters;
}
},
actions: {
async fetchCharacters({ commit }) {
await plainAxiosInstance
.get('/characters')
.then(response => {
let characters = response.data;
commit('SET_CHARACTERS', characters);
console.log(characters); <-- Logs the expected response data
return characters;
})
.catch(error => console.log('Failed to fetch characters', error));
}
},
getters: {}
};
export default characterModule;
I am then dispatching the Vuex action inside of a Vue component on mount:
<script>
import { mapState, mapActions } from 'vuex';
export default {
mounted() {
this.fetchCharacters()
.then(response => {
console.log('response', response);
// response is logging undefined here <----
})
.catch(error => {
console.log(error);
});
},
computed: mapState(['character']),
methods: mapActions('character', ['fetchCharacters'])
};
</script>
The console.log in modules/character.js logs the data as expected and then the response inside of the component logs undefined. I made sure to return the variable characters in the Vuex module. And I also made the Vuex action fetchCharacters async. So why is the response in the component returning undefined?
Thanks if you can help.
Change this:
async fetchCharacters({ commit }) {
await plainAxiosInstance
to this:
fetchCharacters({ commit }) {
return plainAxiosInstance
You can keep the async if you want but it won't make any difference.
In its present form the action will implicitly return a promise and that promise won't resolve until the request is complete. However there's nothing to tell it to resolve that promise to the desired value.
Instead of waiting for the promise inside the action you can just return that promise instead. Externally that won't make any difference as you'll just get back a promise either way but crucially that promise will resolve to the correct value once the request is complete.
I have a react + typescript application and I have an async api call done with axios. I want to test that async call using Jest + Enzyme.
This is what I have for my action
// load items callback
export function loadItemsSuccess(response) {
return {
type: LOAD_ITEMS,
items: response.data
};
}
// load items
export function loadItems() {
return function (dispatch) {
const authOptions = {
method: "GET",
url: "http://192.168.99.100:3000/items
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
json: true
};
return axios(authOptions).then(response => {
dispatch(loadItemsSuccess(response));
}).catch(error => {
console.log("Error loading items, error);
});
};
}
My reducer simple updates the store:
case LOAD_ITEMS:
console.log(action.items);
return action.items;
Unit testing the action should test whether the expected object is dispatched.
One way to do this is to use a combination of redux-mock-store and axios-mock-adapter.
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
// import any actions and types
const middleware = [thunk]
const mockStore = configureMockStore(middleware)
describe('loadItems', () => {
it('Dispatches LOAD_ITEMS', () => {
let mock = new MockAdapter(axios)
mock
.onGet('http://192.168.99.100:3000/items')
.reply(200, { data: 'mock' })
const store = mockStore({})
return store.dispatch(actions.loadItems())
.then(() => {
const actions = store.getActions()
expect(actions[0]).toHaveProperty('type', LOAD_ITEMS)
})
})
})
Reducers and the values in the store should be tested as a separate unit. We're just using the mockStore to dispatch the action and ensure the proper type is dispatched. MockAdapter mocks network requests. This way we can create tests around various network conditions such as 400s and timeouts.
Axios Mock Adapter
Redux Mock Store
I use HTTP requests to get data for my Vue.js application. I have one file called Api.js with the base axios instance:
export default () => {
return axios.create({
baseURL: apiURL,
headers: {
Authorization: `JWT ${store.state.token}`
}
})
}
than I have a file called service.js, which contains the functions for the different endpoints:
export default {
status() {
return Api().get('status/')
}
}
In the .vue file I call the method like that.
created() {
Service.status()
.then(response => {
// do something with the data
})
.catch(e => {
// show exception
})
}
Some exceptions should be handled in Api.js (for example: 401), some other exceptions should be handled in service.js and others in the .vue file. How can I do that?
Disclaimer: I have created two small axios plugins to achieve this specific pattern easily.
axios-middleware
Simple axios HTTP middleware service to simplify hooking to HTTP requests made through Axios.
It uses axios interceptors as mentioned by acdcjunior but it abstracts the use of axios with a commonly known middleware pattern so your app doesn't need to know and deal with the interceptor syntax.
// import your API's axios instance
import http from './api';
import { Service } from 'axios-middleware';
// Create a new service instance
const service = new Service(http);
// We're good to go!
export default service;
You can then use this middleware service to register different middlewares anywhere in your app. A middleware can be as simple as an object or a reusable, easily testable class.
import i18n from './services/i18n';
import toast from './services/toast';
import service from './services/middleware';
import { ApiErrorMiddleware, OtherMiddleware } from './middlewares';
// Then register your middleware instances.
service.register([
// Middleware class instance
new ApiErrorMiddleware(i18n, toast),
new OtherMiddleware(),
// or a simple object
{
onRequest() {
// handle the request
},
onResponseError(error) {
// handle the response error
}
}
]);
Where the ApiErrorMiddleware would be a simple class with the sole responsibility of showing toast messages on error.
export default class ApiErrorMiddleware {
/**
* #param {VueI18n} i18n instance
* #param {Object} toast message service
*/
constructor(i18n, toast) {
this.toast = toast;
this.i18n = i18n;
}
/**
* #param {Object} error
*/
onResponseError(error = {}) {
const { response } = error;
let key = 'errors.default';
if (response && this.i18n.te(`errors.${response.status}`)) {
key = `errors.${response.status}`;
} else if (error.message === 'Network Error') {
key = 'errors.network-error';
} else {
// TODO log unhandled errors
}
this.toast.error(this.i18n.t(key));
}
}
axios-resource
Simple axios resource class to easily interact with a REST endpoint.
Define a resource class. Here, I added onError and onFetchError as examples for your use-case.
import Resource from 'axios-resource';
export default class UserResource extends Resource {
static URL = 'user/{id}';
// This calls `sync` in the background
fetch() {
return super.fetch.apply(this, arguments)
.catch(err => this.onFetchError(err));
}
onFetchError(err) {
// An error occurred while fetching this resource.
}
onError(err) {
// An error occurred with this resource
}
// called for every actions (fetch, create, patch, delete)
sync() {
return super.sync.apply(this, arguments)
.catch((err) => this.onError(err))
}
}
Then, in api.js, create an instance.
import UserResource from './user';
const user = new UserResource();
// GET https://example.com/api/user/me
user.fetch('me')
.then(({ data }) => {
console.log('User data:', data);
});
The error can be dealt with at every step.
in the onFetchError of this specific resource
in the onError of this resource
in a middleware for the app.
You should add axios interceptors:
Axios Interceptors
You can intercept requests or responses before they are handled by
then or catch.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
Those can (should) be in your Api.js.
I'm using axios to do a post in my action creator. My action creator takes in an array of objects and posts it to an express server.
I'm trying to set the payload to be the response of the server but I realize that the response occurs after the action has been created.
export function sendOrders (data : Order[] ){
console.log("gets inside sendOrders action creator")
var output : string;
axios.post('http://localhost:8081/', data)
.then(function (response) {
output = response.data;
console.log(output)
})
.catch(function (error) {
output = error;
console.log(output)
});
return {
type: SEND_ORDERS,
payload : output
}
}
As a result, my reducer returns "undefined". Does anyone know how I can work around this?
Action creators are synchronous. That being said, there are redux plugins, like redux-thunk and redux-saga that allow an action creator to be asynchronous, which would let you emit an action after the Promise completes.
Example using redux-thunk:
export function sendOrders (data : Order[]) {
return (dispatch) => {
var output : string;
return axios.post('http://localhost:8081/', data)
.then(function (response) {
output = response.data;
console.log(output)
dispatch({
type: SEND_ORDERS,
payload: output
});
})
.catch(function (error) {
output = error;
console.log(output)
});
};
}
So essentially, your action creator returns a Promise-returning function which is passed dispatch and getState so you can asynchronously read state and dispatch actions. In order to use this example, you have to add the plugin. From the sample in the documentation:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);