React + Redux: Wait for async action resolves - javascript

I'm new into the React and Redux worlds and after a lot of research, I haven't found a way to handle the problem I have:
I need to perform an api call on app init, but the endpoint is in a configuration file. This configuration in the server so it has to be downloaded and read. This is because I need to distribute the app into many servers and each server has a different configuration.
Therefore the api call has to wait until the configuration has been loaded, they must be chained.
I'm using Redux to manage the state of the app so I have an action which downloads the configuration and an other action which performs the api call.
// Config action
export function fetchConfigRequest() {
return {
type: types.FETCH_CONFIG_REQUEST
}
}
export function fetchConfigSuccess(config) {
return {
type: types.FETCH_CONFIG_SUCCESS,
config
}
}
export function fetchConfig() {
return dispatch => {
dispatch(fetchConfigRequest());
return axios.get('config.json')
.then(response => {
dispatch(fetchConfigSuccess(response.data));
})
;
};
}
// Api client action
export function fetchDataRequest() {
return {
type: types.FETCH_DATA_REQUEST
}
}
export function fetchDataSuccess(data) {
return {
type: types.FETCH_DATA_SUCCESS,
data
}
}
export function fetchDataError(error) {
return {
type: types.FETCH_DATA_ERROR,
error
}
}
export function fetchData(filters = {}) {
return dispatch => {
dispatch(fetchDataRequest());
const apiClient = new apiClient({
url: state.config.apiEndpoint
});
return apiClient.Request()
.then(response => {
dispatch(fetchDataSuccess(data));
})
;
};
}
The only way that I got it working is by waiting until config action promise resolves in App component like this:
// App.component.js
componentDidMount() {
this.props.fetchConfig().then(() => {
this.props.fetchData();
});
}
But I don't think this is the best and the most "Redux style" way to do it, so how should I do it?
I've some ideas in my mind but I don't know what would be the best one:
Keep it as it is now
Create an 'app' action which dispatches the fetch config action, waits until config is loaded, and then dispatches the fetch data action
Do it into a custom middleware
Thanks!

Related

RxJS poll server and update application state with new data

The server node.js updates data every 0.5 seconds. The client has to poll the server and fetch new data using RxJS. I have made the client to poll server, the requests are made but I cant read the response from the server. I think that the state is not updated due to poll_server returning timer.pipe() or the reducer is creating a wrong state. I came to this from my teacher's template, so why the dispatcher would return an Observable?
model.js
export function init_state(warnings) {
return {
warnings,
accept: ({ visit_site }) => { if (visit_site) return visit_site(warnings) }
}
}
dispatcher.js
import { from, of, timer } from 'rxjs'
import { concatMap, map } from 'rxjs/operators'
import { FRONT_PAGE } from './constants'
const poll_server = url => {
return timer(0, 3000)
.pipe(concatMap(() => from(fetch(url))
.pipe(map(response => response.json())))
)
}
export const server_dispatch = action => {
switch (action.type) {
case FRONT_PAGE: {
const res = poll_server('http://localhost:8080/warnings')
console.log(res)
return res
}
default:
return of(action)
}
}
reducer.js
export function reduce(state, action) {
switch (action.type) {
case FRONT_PAGE:
console.log(`REDUCER CALLED WITH ACTION ${FRONT_PAGE}`)
return init_state(action)
default:
return state
}
}
The problem is that you want to call poll_server which will start an observable stream on an ajax endpoint from within the reducer (which implies you would subscribe to this stream here, not the way you are currently using it), which isn't how redux is supposed to be used. Reducers are intended by its creator to be pure functions without causing side effects.
If you want to use redux with observables, it is advised to use custom middleware to handle these side effects. The most obvious middleware I suggest is https://redux-observable.js.org/, which I've tried in the past and works without trouble.
The documentation is exquisite, and you will use this without trouble if you are already familiar with RX principles.

Vue.js import Axios within method then use it

I have got a component which might not request an ajax call if some data has been passed into it. However if the data hasn't been passed in I need to fetch it, so I want to import Axios then, save importing it for no reason.
How can I wait for the script to be imported before attempting to use it, as the below doesn't work:
export default {
props: {
vehicleId: {
type: Number|String,
required: true,
default: () => null
},
settings: {
type: Object,
default: () => null
}
},
beforeCreate() {
if (!this.settings) {
const Axios = () => import('../../../axiosConfig');
Axios.get('/api/v1/media-slider-settings').then(response => {
this.settings = response.data;
});
}
},
Dynamic import return a Promise, so you must to use then function.
Try something like that:
<script>
export default {
beforeCreate() {
if (!this.settings) {
import('../../../axiosConfig').then(axios => {
axios.get('/api/v1/media-slider-settings').then(response => {
this.settings = response.data;
});
});
}
},
};
</script>
Avoid the approach with async/await because the lifecycle functions don't support asynchronous in Vue.js.
You're almost there, import() is async, so just do:
// or use .then if you're not in an async function
const Axios = (await import('../../../axiosConfig')).default
Axios.get('/api/v1/media-slider-settings').then(response => {
this.settings = response.data;
});
and notice that import() returns the module, so you need to get the .default property if you need the default export (like in your case) or just call .someExportedName for importing a named export (i.e. non-default export from the module)

Cancel previous fetch request with redux-thunk

Background of the Problem:
I am building a React/Redux app that uses redux-thunk and wretch (a fetch wrapper) to handle asynchronous requests.
I have a few search actions that can vary significantly in their load times, causing undesirable behavior.
I have looked into using AbortController(), but it's either cancelling all my requests outright, or failing to cancel the previous request.
example problem:
Request a search for "JOHN", then request a search for "JOHNSON".
Results for "JOHNSON" return first, and then results for "JOHN" return later and overwrite the "JOHNSON" results.
Goal:
Initiating a request should abort previous pending requests.
example desired behavior:
Request a search for "JOHN", then request a search for "JOHNSON".
Upon initiating the request for "JOHNSON", the pending request for "JOHN" is aborted.
Code:
actions.js
The fetchData action gets called via an onClick or by other functions.
import api from '../../utils/request';
export function fetchData(params) {
return dispatch => {
dispatch(requestData());
return api
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData() {
return {
type: REQUEST_DATA,
waiting: true,
}
}
export function receiveData(items, totalItems) {
return {
type: RECEIVE_DATA,
result: items,
totalItems: totalItems,
waiting: false,
}
}
export function apiFail(err) {
return {
type: API_FAIL,
error: err,
waiting: false,
}
}
utils/request.js
This is wretch import. Wretch is a fetch wrapper so it should function similarly to fetch.
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const api = wretch()
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
export default api;
Attempt:
I've tried using wretch's .signal() parameter with an AbortController(), calling .abort() after the request, but that aborts all requests, causing my app to break. Example below:
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const controller = new AbortController();
const api = wretch()
.signal(controller)
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
controller.abort();
export default api;
I've tried moving the logic around to various places, but it seems abort all actions or abort none of them.
Any advice as to how to go about this would be appreciated, this is critical for my team.
Thank you
I feel pretty silly right now, but this is what it took to get it working.
Solution Steps:
Set an AbortController to the initialState of the reducer
reducer.js
export default (state = {
controller: new AbortController(),
}, action) => {
switch (action.type) {
...
Get the AbortController from the state, at the beginning of the fetch action and abort it.
Create a new AbortController and pass it into the requestData action.
Pass the new AbortController into the signal() param of the wretch call.
actions.js
export function fetchData(params) {
return (dispatch, getState) => {
const { controller } = getState().reducer;
controller.abort();
const newController = new AbortController();
dispatch(requestData(newController));
return api
.signal(newController)
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData(controller) {
return {
type: REQUEST_DATA,
waiting: true,
controller,
}
}
In the reducer, for the case of the requestData action, set the new AbortController to the state.
reducer.js
case REQUEST_DATA:
return {
...state,
waiting: action.waiting,
controller: action.controller
};
There's some additional functionality with wretch, an .onAbort() param, that allows you to dispatch other actions when the request is aborted. I haven't coded that out yet, but I figured I'd include the info for anyone else struggling with this.

Rerun function on error and pass result up

I have separated my api call into three layers. The component, the repository, and the apihelper. I want the logic for refresh_tokens to be in apihelper.js. When I do this it seems like the apihelper runs again after getting the 401 response status but it never passes the data back up to the component. I know I could put the logic to rerun it in the component but that seems like it will end up being a lot of duplicate code as I add more calls. I feel like it's probably caused by my shallow understanding of javascript promises but I'm a javascript beginner.
Component
<script>
import breweryrepository from '#/repository/breweryrepository.js'
export default {
mounted() {
this._getTotalBreweries();
},
methods: {
_getTotalBreweries() {
breweryrepository.getTotalBreweries()
.then((response) => {
if(response.data)
{
this.totalNumberOfBreweries = response.data.totalBreweries;
}
})
}
},
data () {
return {
totalNumberOfBreweries: ''
}
}
}
</script>
Repository
import apihelper from '#/helpers/ApiHelper.js';
export default {
getTotalBreweries() {
return new Promise(function(resolve, reject) {
resolve(apihelper.apiCall('/brewery/totalnumber'));
});
}
}
Apihelper
import axios from 'axios';
var querystring = require('querystring');
import { store } from '../store/store.js';
import auth from '#/auth/auth.js'
export default {
apiCall(url) {
return axios.get(store.state.baseUrl + url, { 'headers': auth.getAuthHeader() })
.catch((error) => {
if(error.response.status == 401)
{
console.log("401 error, running refresh and apicall again");
auth.refreshToken();
this.apiCall(url);
}
})
}
}
Aaaaand I wasn't returning the call.
return this.apiCall(url);
Works now

Component in Vue.js server-side rendering

I am trying to make my Vue app have server-side rendering. I am using vue-server-renderer (https://www.npmjs.com/package/vue-server-renderer). Client-side rendering is working fine.
My app use vue-router and axios
Here is my server.js:
server.get('*', (request, response) => {
bundleRenderer.renderToString({ url: request.url }, (error, htmlPromise) => {
if (error) {
// Log the error in the console
console.error(error)
// Tell the client something went wrong
return response
.status(500)
.send(error)
}
response.send(layout.replace('<div id=app></div>', htmlPromise))
})
})
getInfo() is the method to fetch server data.
Here is getInfo():
export default {
methods: {
getInfo(api) {
return axios
.get(api || this.$route.params.path)
.then((data) => {
this.data = data
this.$set(this, 'isLoading', false)
})
},
},
}
My server entry is:
import { app, router, store } from './index'
export default context => {
let componentPromises = router.getMatchedComponents().filter((component) => {
return component.methods && component.methods.getInfo
}).map((component) => {
return component.methods.getInfo()
})
return Promise.all(componentPromises).then(() => {
return app
})
}
However, I soon realize that all the components from router.getMatchedComponents() does not have $route or $set. Therefore, the method getInfo() stops working.
The document from https://router.vuejs.org/en/api/router-instance.html is very short and does not provide much information:
router.getMatchedComponents()
Returns an Array of the components (definition/constructor, not
instances) matched by the current route. This is mostly used during
server-side rendering to perform data prefetching.
How can I fix the problem?
I have previously incurred into a similar problem and managed to successfully prefetch data by doing the following:
app.$router.onReady(() => {
const matchedComponents = app.$router.getMatchedComponents()
if (!matchedComponents.length) { /* ... */}
Promise.all(matchedComponents.map((Component: any) => {
if (Component.options.methods.asyncData) {
return Component.options.methods.asyncData({
store: app.$store,
route: app.$router.currentRoute
});
}
})).then(() => { /* your callback here ... */ });
}
According to vue ssr documentation (https://ssr.vuejs.org/en/data.html) the suggested way is to use a custom asyncData method in your component to perform data fetching rather than calling component methods directly:
export default {
asyncData ({ store, route }) {
// return the Promise from the action
return store.dispatch('fetchItem', route.params.id)
}
},

Categories