I am using Axios, Redux and Redux Promise Middleware.
I have the following action creator:
return {
type: 'FETCH_USERS',
payload: axios.get('http://localhost:8080/users',
{
params: {
page
}
}
)
}
In my reducer for the FETCH_USERS_PENDING action, I would like to save the requested url in my state. How can I do this?
You want to use redux-promise-middleware's "meta" variable. Like so:
return {
type: 'FETCH_USERS',
meta: { url: 'http://localhost:8080/users' },
payload: axios.get('http://localhost:8080/users', config)
}
You could pass it through in your params, but that won't be returned until the page is fetched. Which means it won't be passed back during FETCH_USERS_PENDING.
And I'm pretty sure if you include directly in the return object (like how Lucas suggested), it will be stripped out of the FETCH_USERS_PENDING stage.
Here's the FETCH_USERS_PENDING stage from redux-promise-middleware:
/**
* First, dispatch the pending action. This flux standard action object
* describes the pending state of a promise and will include any data
* (for optimistic updates) and/or meta from the original action.
*/
next({
type: `${type}_${PENDING}`,
...(data !== undefined ? { payload: data } : {}),
...(meta !== undefined ? { meta } : {})
});
As you can see during this stage, the middleware returns the appended "type" attribute and it checks for "data" & "meta" attributes. If present, they are passed along within the action.
Here's the redux-promise-middleware source code if you want to look into it further.
Simply pass the URL in the action as well:
return {
type: 'FETCH_USERS',
url: 'http://localhost:8080/users',
payload: axios.get('http://localhost:8080/users', {
params: {
page
}
}
}
and access it in the reducer with action.url. Or you can leave the action as it is, and access it in the promise resolution:
action.payload.then(function(response) {
var url = response.config.url;
});
Related
When using RTK-Query to call an API, I need to access a custom header value. I've tried various options for accessing the response headers from the documentation, but all attempts result in an empty object returned:
meta: {
"request": {},
"response":{}
}
I can see in the network tab, that the headers are provided in the response. This is part of a refactor from using raw Axios calls where the header was available from the response object as the headers property.
I've tried accessing the headers through the meta parameter on the transformResponse function of the createApi
transformResponse: (response, meta, arg) => {
console.log(`Transform -> meta: ${JSON.stringify(meta)}`)
// dataKey: meta.response.headers['x-company-data-key'],
return {
...response,
// dataKey
}
}
I've also tried accessing the headers via the meta property of the action parameter from the extraReducers function in the feature slice:
extraReducers: (builder) => {
builder.addMatcher(companyApi.endpoints.getSomeData.matchFulfilled, (state, action) => {
console.log(`action meta: ${JSON.stringify(action.meta)}`)
state.result = action.payload.result
// state.dataKey = action.meta.response.headers['x-company-data-key']
})
}
Both instances result in the meta object that looks like this:
meta: {
"request": {},
"response": {}
}
The API's base query looks like this:
baseQuery: fetchBaseQuery({
baseUrl: process.env.REACT_APP_API_ENDPOINT,
prepareHeaders: ( (headers, { getState }) => {
const realm = getState().companyAuth.realm
if (realm) {
const token = readToken(realm)
if (token)
headers.set('Authorization', `Bearer ${token.access_token}`)
}
return headers
})
}),
and finally, the endpoints definition for this API endpoint:
getCompanyData: builder.query({
query: (params) => {
const { location, page, pageSize } = params
return {
url: `/companies/${location}`,
params: { page, pageSize }
}
}
})
Based on what I could find in the documentation, GitHub issues, and PRs, it seems that the meta property should automatically add the headers, status, and other additional HTTP properties. I'm not sure what I've missed where the response object is completely empty. Is there an additional setting I need to add to the baseQuery?
I should note, that the response data is working properly. I just need the additional information that is returned in the custom header.
I would like show 404 page if blog/xyz don't work. So i've add dataNotFound on my routes.js, but i've no result :
Router.route('/blog/:slug', {
name: 'blogPost',
parent: 'blog',
itemName: function () {
return this.data().post.title;
},
data: function () {
let post = Posts.findOne({
'slug': this.params.slug
});
return {
post,
profil
};
}
});
Router.onBeforeAction('dataNotFound', {
only: 'blogPost'
});
If i test wrong url with blog/ojhojeofje, i don't have 404 page, just post without data.
Do you have any idea ?
Thank you !
First of all, you need to register dataNotFound as plugin instead of in onBeforeAction:
Router.plugin('dataNotFound', { only: ['blogPost'] });
Secondly the dataNotFound plugin works by checking if your route data() returns a falsy value. Since you want to load multiple data object in your data() function, you need to alter your function so that it will return something falsy if the post is not found. For example you can simply do this:
data: function () {
let post = Posts.findOne({
'slug': this.params.slug
});
if (!post) {
return false;
}
...
Note that you also need to make sure that your subscription to the Posts collection is ready before you run data in order to avoid going to the not found page unnecessarily.
I'm using Iron Router. I have a RouterController that looks something like this:
var loggedInUserController = RouteController.extend({
layoutTemplate: "GenericLayout",
waitOn: function () {
return Meteor.subscribe("TheDataINeed");
}
});
And I have a route defined which uses this controller to wait for the 'TheDataINeed':
Router.route("/myapp", {
name: "Landing",
controller: loggedInUserController,
data: function () {
if(this.ready()){
return {content: "page-landing"};
}
}
});
Now, the problem is the data I am subscribed to is conditional: meaning, depending on the user's role, I publish different data, like so:
if (!Roles.userIsInRole(this.userId, 'subscribed') ) {
return [
myData.getElements({}, { fields: { _id: 1, title: 1}, limit: 5 })
];
} else {
return [
myData.getElements({}, { fields: { _id: 1, title: 1} })
];
}
When the user's role is not 'subscribed', I limit the published data to 5 elements.
The problem is publishing is not reactive, so when the user changes his role for the first time to 'subscribed' and I navigate to my route ("/myapp"), the user still sees the limited number of elements instead of all of them.
Is there a way to manually re-trigger the subscription when I am loading this route? If possible, I'd like to do this without adding new packages to my app.
Not sure about that approach but can you try to set session value in route instead of subscription code. Then in a file on client side where your subscriptions are you can wrap Meteor.subscribe("TheDataINeed") in Tracker.autorun and have a session as a subscription parameter. Every time that session value is changed autorun will rerun subscription and it will return you data based on a new value.
I have decided to not use ember-data as it's not production ready and still changing. My app only needs to make a few ajax requests anyway so it shouldn't make too big of a difference. I am having trouble understanding how to handle an ajax promise response.
When my user loads the app they already have an authenticated session. I am trying to ping the server for that users info and display it in my template. It seems my template is rendered before my ajax request returns results and then does not update with the promise.
// route
App.ApplicationRoute = Ember.Route.extend({
setupController: function(){
this.set("currentUser", App.User.getCurrentUser());
}
});
// model
App.User = Ember.Object.extend({
email_address: '',
name_first: '',
name_last: '',
name_full: function() {
return this.get('name_first') + ' ' + this.get('name_last');
}.property('name_first', 'name_last')
});
App.User.reopenClass({
getCurrentUser: function() {
return $.ajax({
url: "/api/get_current_user",
type: "POST",
data: JSON.stringify({})
}).then(function(response) {
return response;
});
}
});
In my template:
<h1> Hey, {{App.currentUser.name_first}}</h1>
How would I update the template when I receive a response or delay rendering until I have a response?
Actually the answer is quite easy: You do not need to use a promise. Instead just return an empty object. Your code could look like this:
App.User.reopenClass({
getCurrentUser: function() {
var user = App.User.create({}); //create an empty object
$.ajax({
url: "/api/get_current_user",
type: "POST",
data: JSON.stringify({})
}).then(function(response) {
user.setProperties(response); //fill the object with your JSON response
});
return user;
}
});
What is happening here?
You create an empty object.
You make an asynchronous call to your API...
... and in your success callback you fill your empty object.
You return your user object.
Note: What is really happening? The flow mentioned above is not the sequence in which those actions are happening. In reality the points 1,2 and 4 are performed first. Then some time later, when the response returns from your server, 3 is executed. So the real flow of actions is: 1 -> 2 -> 4 -> 3.
So the general rule is to always return an object that enables Ember to do its logic. No values will be displayed first in your case and once your object is filled Ember will start do its magic and auto update your templates. No hard work needs to be done on your side!
Going beyond the initial question: How would one do this with an array?
Following this general rule, you would return an empty array. Here a little example, which assumes, that you might like to get all users from your backend:
App.User.reopenClass({
getAllUsers: function() {
var users = []; //create an empty array
$.ajax({
url: "/api/get_users",
}).then(function(response) {
response.forEach(function(user){
var model = App.User.create(user);
users.addObject(model); //fill your array step by step
});
});
return users;
}
});
I'd use Ember.Deferred instead of returning an empty array as mentioned before.
App.User.reopenClass({
getAllUsers: function() {
var dfd = Ember.Deferred.create();
var users = [];
$.ajax({
url: "/api/get_users",
}).then(function(response) {
response.forEach(function(user){
var model = App.User.create(user);
users.addObject(model);
});
dfd.resolve(users);
});
return dfd;
}
});
In your model hook all you have to do is this
model: function(){
return App.User.getAllUsers();
}
Ember is smart enought and knows how to handle the promise you return, once it's resolved the model will be correctly set, you can also return a jQuery promise but it will give you some weird behavior.
You can as well set the current user as the model for your ApplicationRoute like so:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return App.User.getCurrentUser();
}
});
Since getCurrentUser() returns a promise, the transition will suspend until the promise either fulfills or rejects.
This is handy because by the time transition is finished your model is initialized and you will see it rendered in the template.
You can read up more about async routing in Ember guides.
I'm new to using redux, and I'm trying to set up redux-promise as middleware. I have this case I can't seem to get to work (things work for me when I'm just trying to do one async call without chaining)
Say I have two API calls:
1) getItem(someId) -> {attr1: something, attr2: something, tagIds: [...]}
2) getTags() -> [{someTagObject1}, {someTagObject2}]
I need to call the first one, and get an item, then get all the tags, and then return an object that contains both the item and the tags relating to that item.
Right now, my action creator is like this:
export function fetchTagsForItem(id = null, params = new Map()) {
return {
type: FETCH_ITEM_INFO,
payload: getItem(...) // some axios call
.then(item => getTags() // gets all tags
.then(tags => toItemDetails(tags.data, item.data)))
}
}
I have a console.log in toItemDetails, and I can see that when the calls are completed, we eventually get into toItemDetails and result in the right information. However, it looks like we're getting to the reducer before the calls are completed, and I'm just getting an undefined payload from the reducer (and it doesn't try again). The reducer is just trying to return action.payload for this case.
I know the chained calls aren't great, but I'd at least like to see it working. Is this something that can be done with just redux-promise? If not, any examples of how to get this functioning would be greatly appreciated!
I filled in your missing code with placeholder functions and it worked for me - my payload ended up containing a promise which resolved to the return value of toItemDetails. So maybe it's something in the code you haven't included here.
function getItem(id) {
return Promise.resolve({
attr1: 'hello',
data: 'data inside item',
tagIds: [1, 3, 5]
});
}
function getTags(tagIds) {
return Promise.resolve({ data: 'abc' });
}
function toItemDetails(tagData, itemData) {
return { itemDetails: { tagData, itemData } };
}
function fetchTagsForItem(id = null) {
let itemFromAxios;
return {
type: 'FETCH_ITEM_INFO',
payload: getItem(id)
.then(item => {
itemFromAxios = item;
return getTags(item.tagIds);
})
.then(tags => toItemDetails(tags.data, itemFromAxios.data))
};
}
const action = fetchTagsForItem(1);
action.payload.then(result => {
console.log(`result: ${JSON.stringify(result)}`);
});
Output:
result: {"itemDetails":{"tagData":"abc","itemData":"data inside item"}}
In order to access item in the second step, you'll need to store it in a variable that is declared in the function scope of fetchTagsForItem, because the two .thens are essentially siblings: both can access the enclosing scope, but the second call to .then won't have access to vars declared in the first one.
Separation of concerns
The code that creates the action you send to Redux is also making multiple Axios calls and massaging the returned data. This makes it more complicated to read and understand, and will make it harder to do things like handle errors in your Axios calls. I suggest splitting things up. One option:
Put any code that calls Axios in its own function
Set payload to the return value of that function.
Move that function, and all other funcs that call Axios, into a separate file (or set of files). That file becomes your API client.
This would look something like:
// apiclient.js
const BASE_URL = 'https://yourapiserver.com/';
const makeUrl = (relativeUrl) => BASE_URL + relativeUrl;
function getItemById(id) {
return axios.get(makeUrl(GET_ITEM_URL) + id);
}
function fetchTagsForItemWithId(id) {
...
}
// Other client calls and helper funcs here
export default {
fetchTagsForItemWithId
};
Your actions file:
// items-actions.js
import ApiClient from './api-client';
function fetchItemTags(id) {
const itemInfoPromise = ApiClient.fetchTagsForItemWithId(id);
return {
type: 'FETCH_ITEM_INFO',
payload: itemInfoPromise
};
}