amplitude / redux-query Goto Github PK
View Code? Open in Web Editor NEWA library for managing network state in Redux
Home Page: https://amplitude.github.io/redux-query
License: Other
A library for managing network state in Redux
Home Page: https://amplitude.github.io/redux-query
License: Other
It took me a minute to get redux-query working with a redux-immutable store, so I thought I'd share. Apologies if this is the wrong venue.
Basically the lowest-surface area solution seemed to be to just wrap the provided redux-query reducers with some immutable serialization logic.
Define reducers/entities.js
as:
import Immutable from 'immutable';
import { entitiesReducer } from 'redux-query';
// wrap the redux-query reducer immutable serialization logic
const wrappedEntitiesReducer = function(state = Immutable.Map(), action) {
return Immutable.fromJS(entitiesReducer(state.toJS(), action));
};
export default wrappedEntitiesReducer;
Then reducers/queries.js
as:
import Immutable from 'immutable';
import { queriesReducer } from 'redux-query';
// wrap the redux-query reducer immutable serialization logic
const wrappedQueriesReducer = function(state = Immutable.Map(), action) {
return Immutable.fromJS(queriesReducer(state.toJS(), action));
};
export default wrappedQueriesReducer;
Then use your wrapped versions when you call combineReducers:
import { combineReducers } from 'redux-immutable';
import entities from 'reducers/entities';
import queries from 'reducers/queries';
const rootReducer = combineReducers({
...etc,
entities,
queries
});
export default rootReducer;
And of course, the middleware selector functions in store.js
:
const getQueries = state => state.get('queries');
const getEntities = state => state.get('entities');
It would be more efficient to have built-in immutable support, but absent that I was happy enough to just get it working.
Created from #5 (comment)
The issue occurs with connectRequest
when query key for the query config returned from mapPropsToConfig
is derived from redux state that changed from the request finishing.
Pretty sure this is a race condition due to query middleware dispatching the REQUEST_SUCCESS event before the requestAsync promise is resolved. Since connectRequest has its own state for pending requests, it can get out of sync and cause cancel actions to be dispatched.
Our app can load the same entity in two ways: by its ID, or by loading a list with that entity.
I want to avoid loading the single entity after having loaded the list view, even though redux-query will see them as different routes. Redux-query cannot know the relationships between my routes and adjust its cache strategy for them - that's fine.
My solution is simple:
I'm using mapState to determine whether or not a component has all the data it needs to render, which it then lets redux-query and the component know by attaching a loading
prop.
My connect request mapPropsToConfig
then only fetches if loading is true.
This reduces redundant requests, but does allow a few cases where data can still be fetched (in the case of coordinating loading many different entity types from singly typed routes, á la REST, the order the requests come back in can effect rendering during the app's bootstrapping phase).
Hi,
I'm using version 1.5 of redux-query-immutable, which just makes the library work with an immutable store, but adds no other logic.
When a component uses connect-request
and gets unmounted, while a query is in flight, it correctly tries to cancel the query. Here I always get an error, because the request object in the store has no abort()
method. This is due to the following lines of code:
https://github.com/amplitude/redux-query/blob/v1.5.0/src/middleware/query-advanced.js#L112
https://github.com/amplitude/redux-query/blob/v1.5.0/src/middleware/query-advanced.js#L215
The tests pass, because we artificially create the request object in the store with an abort()
function, but with request.instance
, only the bare request object is passed to the redux store. I think the whole request
object should be stored, not request.instance
.
Why we not call reject when request get the error from the server?
https://github.com/amplitude/redux-query/blob/master/src/middleware/query-advanced.js#L95
Noticing that several of the functions exported from index.js
have no corresponding documentation as far as I can tell. Would be useful to have them on in the readme.md
.
We should support the same meta
field for mutate actions that we do for request acitons.
After installing and configuration I got error
Bundling
index.android.js
Transforming modules ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░ 47.8% (264/382)...(node:33907) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3676): SyntaxError: TransformError: /Users/vomchik/Projects/node_modules/redux-query/src/index.js: [BABEL] /Users/vomchik/Projects/node_modules/redux-query/src/index.js: Unknown option: /Users/vomchik/Projects/node_modules/react/react.js.Children. Check out http://babeljs.io/docs/usage/options/ for more information about options.
A common cause of this error is the presence of a configuration options object without the corresponding preset name. Example:
Invalid:
{ presets: [{option: value}] }
Valid:
{ presets: [['presetName', {option: value}]] }
For more detailed information on preset configuration, please see http://babeljs.io/docs/plugins/#pluginpresets-options.Bundlingindex.android.js
Any ideas?
Hi,
I created a fork for this great library, that works with a redux store based on ImmutableJS. If somebody is interested please checkout redux-query-immutable.
v1
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connectRequest(() => [createGetCoursesQuery(), createGetUserQuery()])
)(PageHome);
v2
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connectRequest(() => createGetCoursesQuery()),
connectRequest(() => createGetUserQuery())
)(PageHome);
Only one request is sent and I can't make both of them work. THere's a line in docs about support of array in connectRequest
, so maybe I'm just using it wrong.
v 1.2.0
current description
Use the connectRequest higher-order component to declare network dependencies for a React component. connectRequest takes a function that transforms the component props to a request query config or an array of request query configs.
A common use case in an app I'm building is fetching data for a listing view using a bulk ID endpoint, then transitioning to a detail page.
/api/posts?ids=1,2,3
/api/posts?ids=1
It would be wonderful if redux-query exposed a custom method on the adapter (or anywhere else) to allow me to tell it what is or isn't a duplicate request. Right now, we're suffering a request for multiple IDs and then another for a single ID when the user transitions to the detail page.
P.S. love the library and amazing work.
First: great work on this library - I'm throwing out our app's version of this solution because it isn't as feature complete and because we don't open source things yet.
Thus, I'm being asked about things like cache time to live for responses and cache clearing.
There doesn't seem to be any cache related actions available - do you not run into staleness problems on reads?
It's considered a good practice to avoid creating new functions inside the render
method of a component, as it may cause unnecessary re-renders in child components. That is, avoid using arrow functions or Function.prototype.bind
.
Hey,
Great job putting this library together! I see INFO states something about server side mutations. Do you use this for fetching data on the server as well?
Thanks!
Motivation:
With redux-query 1.x, if you have simultaneous mutations that update the same entity, you could end up in with one mutation completely overwriting the changes of the other one. This is because the entities state for the update is captured before the network request starts, as opposed to when it finishes. Naturally, this has confused several people and just feels wrong.
Solution:
We'll make mutations behave more like requests when it comes to how the entities state is used when updating. transform
s and update
s will always be passed the latest entities state.
Breaking changes:
Related issues:
Motivation:
Currently there's a bug in redux-query 1.x where the middleware is calling abort
on the request instance (i.e. the superagent instance), when the intention is that it should be called on the returned value from the network adapter.
Solution:
Provide a way for external code to reference both the adapter and the underlying instance. We may or may not want to rename "request" to better reflect its purpose.
Breaking changes:
Related issues/PRs:
Motivation:
With redux-query 1.x, mutation failures blindly revert the entire entities state. This shouldn't have much impact for apps that don't have many mutations going on simultaneously, but it's obviously a suboptimal/dangerous behavior.
Solution:
We now need to explicitly handle rollbacks per entity. By default we'll simply revert all optimistically-updated entities back to their previous value before the optimistic update. All entities without an optimistic update handler will be unaffected.
Additionally, we'll offer a custom hook in query configs to enable manual control of the rollback logic. This will enable you to safely support simultaneous mutations for the same entity when using optimistic update. It'll be important that apps consider providing a rollback handler whenever an entity is partially updated by multiple, possibly-simultaneous mutations.
Related issues:
Error
and Rollback
config keys => fns for handling error cases" #9There an edge case with DELETE
request when it is fired twice.
const createRequest = (url, method, body) => {
switch (method) {
// ...
case httpMethods.DELETE:
return superagent.del(url, body);
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
};
When calling superagent.del
with body
param superagent
starts the request. And then we start it for the second time.
At this point of writing i figured out that redux-query
is using quite old version of superagent
lib — 1.6.1 😺 And the latest stable is 3.5.1.
Time to upgrade within v2.0?
That is what I want! Gd job bro! I also want to know that do any case you experienced to integrate saga with redux-query?
We use flow extensively at Amplitude and it'd be great to add flow interfaces to redux-query.
Can you expound on what a network-adapter, is this just redux middleware to redux thunk and sagas? I am setting this up to test out for a project at work and find the current explanation in the README quite unclear.
Thanks
Sorry for that title. I just can't figure out what exactly is wrong.
What is the case:
createQuery1
and createQuery2
functions to get eventscreateQuery1
runs just fine, data is successfully merge to the redux store. Everything is okay.createQuery2
function is going to merge data to the redux store. I'm debugging update
function. And here is the place where the problem lays. The update function does not take current events
values from state (which are there from createQuery1
call) and is empty. But I actually can see data from redux development tools.I'm confused. Any ideas?
Maybe, just maybe it's related to #34 #30 because I store data as an Immutable.js objects.
The README states that you can change a mutation's HTTP method via the options
argument. It doesn't look like that's actually connected. In #10 the options hash is now used in mutations to set the headers.
I can extract the logic from the query action and apply it to the mutation logic as well if you'd like.
Hey, not sure where to look for the problem here. My query definitions look like
{
idAttribute: entity => `${entity.id}`,
url: query,
update: Object.assign({}, {
[resource_name_plural]: (prev, cur) => ({...prev, ...cur})
}),
options: {
credentials: "include",
headers: {
Accept: "application/json",
}
}
}
The problem is that when the entities are retrieved (which they are), they are stored in my Redux state by index, rather than by ID. Note the screenshot:
If I had no need for denormalization, I could make this work. But when I denormalize them (with normalizr.denormalize), it attempts to look the entities up by ID, which breaks (each value for relational data loses its id
and becomes undefined
).
Any ideas?
With redux query, if a user requests an entity which does not exist, how do I let them know?
My suggestion is to add two new functions alongside update
and optimisticUpdate
: error
to handle cases where no client state was optimistically updated, and rollback
to handle undoing optimistic updates.
S = State
S` = newState (State prime)
U = update
OU = optimistic update
E = error
R = Rollback
Successful path where network and back end succeed:
S` = OU(S)
S`` = U(S`)
Failure path where network or back end fails:
S` = OU(S)
S`` = R(S`)
S`` === S // Identity, must be true or else the rollback doesn't roll back
superagent
supports data for GET requests, sending it as query parameters. I was wondering why this ability has been removed in redux-query
's adapter.
Instead of manually sending the body, shouldn't the adapter let superagent handle the body itself?
This change is pretty straight forward. I'd create a PR myself but I wanted to first ask if there's any good reason to not have this already.
Initially, the "entities" reducer state is just an empty object and there doesn't seem to be a way to initialize default state. Consequently my selectors have to check whether state.entities.someEntity
exists — which is cumbersome and verbose.
How do you deal with this situation? Is there a way to set initial state that I'm just not seeing? If not, do you plan to add it?
Thanks!
I've been experimenting replacing our own in-house redux data fetching implementation with redux-query. Fetching single entities or lists of those seem trivial, however I haven't been able to move beyond that so I was hoping you could answer a few questions regarding sorting and pagination.
Let's take a simple real-world example: You're fetching a list of orders from a REST API with three parameters: sortBy
, limit
and skip
.
const OrdersContainer = connectRequest(({ sortBy = 'id', limit = 10, skip = 0}) => ({
url: `/api/orders?sortBy=${sortBy}&limit=${limit}&skip=${skip}`,
queryKey: `orders-sortBy:${sortBy}-limit:${limit + skip}`,
update: {
ordersById: (prevOrders, orders) => ({
...prevOrders,
...orders,
}),
},
}))(OrdersContainer);
In the current version the queries reducer doesn't expose which entities the query resulted in, so I'm unable to know how many orders I've fetched using a given queryKey. Is there another way of doing this?
Request controllers (as returned by network adapters) expose an abort
method which is supposed to be used to interrupt the underlying request. See superagent
adapter for an example.
However, since the addition of the instance
prop (e4b33fb) this controller is no longer stored in the state, and instead its instance
is.
This causes the middleware to call .abort()
on the wrong object.
Say you have a paginated list that uses connectRequest
to load each page. Something like:
const mapPropsToQuery = (props) => ({
url: '/api/users',
body: { page: props.currentPage },
update: { users: updateUsers }
})
export default connectRequest(mapPropsToQuery)(UsersList)
Say UserList
renders a delete button for each user. Upon deletion of a user, the current page and all subsequent pages will need to be re-evaluated.
Current page can be requested again by using forceUpdate
. However, there's no simple way to let redux-query
know that other pages should be fetched again when necessary.
Now React Native 0.44 use React 16 beta.
All react add-ons are was removed from the standard package.
So I get an error when building my project.
error: bundling: UnableToResolveError: Unable to resolve module
react/lib/shallowCompare
from/Users/vomchik/Projects/LITS/toolook/toolook-app-react-native/node_modules/react-addons-shallow-compare/index.js
: Module does not exist in the module map or in these directories:
/Users/vomchik/Projects/LITS/toolook/toolook-app-react-native/node_modules/react/lib
Currently we only support GET/POST/DELETE/PUT. Should be straightforward to support PATCH as well.
1.3.0 breaks backwards-compatibility because the request
in e.g. the REQUEST_START
action is no longer a superagent instance, but just { request, abort }
functions. This breaks any middleware that relied on modifying the superagent instance to centralize e.g. authentication or URL prefixing (as we no longer can use request.use
, for example).
Currently, connectRequest can only make a single request per component per mount, which incentivizes a nice pattern of putting the connectRequest binding to the component that the data is most local to. However, sometimes a component relies on data that cannot be fulfilled by a single request, but the requests don't need to be performed serially (I can see how to use connectRequest + mapState to make iterative requests fairly easily though). This currently isn't supported, but I would like it if the API to do so wouldn't be too confusing, so let's discuss.
The component I had built internally took an array of "requisition" objects which described a URL, a cache key for storage, a normalizing transform, and a dispatch function to call after processing. Perhaps I could simply return an array of config objects from my mapConfigToProps
?
If this is already supported, sorry for not looking at the code this time.
Thanks again for open sourcing this tool and working with me as I integrate it and push against the assumptions it was created with.
How do you test components wrapped in redux-query to ensure your data fetching logic is correct?
Even if my react components are pure, I still test the interactions between state, mapstate, and the pure component's rendering in my tests. When I added connectRequest, I found that jest would throw an error saying "TypeError: requestPromise.then is not a function".
My workaround solution (for Jest testing specifically) is:
connectRequest((props) => {
const {loading} = props // from mapState
if (!loading || !!global.jasmine) {
return undefined
}
return {...config}
})
Where I'm checking for the global jasmine to tell if we're in tests.
Ideally, I'd like a to have jest mock that lets me ensure that the request was attempted (or not), and can be mocked similarly to this superagent mock: jestjs/jest#223 (comment)
Is there a way to set headers requests?
It's common to use a header like Authorization: bearer random-token
for authenticating against a restful API. I saw that there's an options
hash that can be passed along to the middleware, but those only seem to be used for the HTTP method.
If the middleware supported a headers option, or something similar, you could intercept REQUEST_ASYNC
actions with an authorization middleware before it hit middleware/query.js
and merge in any headers your application needs.
Is that a reasonable direction for a pull request? Is there a better solution?
The async example is good because it lets you easily compare to the original version from redux. But it'd be great to build a more complicated example that took advantage of more of redux-query's features:
connectRequest
If we repeat the same request we received an undefined instead normal result from the state.
For better understanding see code above.
First call
const { body } = yield put.resolve(requestAsync({url: '/users'}));
console.log(body) // [{id: 1}, ...]
Second call
const { body } = yield put.resolve(requestAsync({url: '/users'}));
console.log(body) // undefined
I think the problem in this condition https://github.com/amplitude/redux-query/blob/master/src/middleware/query-advanced.js#L94
Almost finished integrating this into the app
This library assumes that all of my entities live together underneath an entities state slice in the redux store.
{
entities: {
user: {},
post: {}
}
}
One thing I want is to be able to use the keys and reducers we've already built out, which are flatter:
{
user: {},
post: {},
... other stuff ...
configuration: {},
}
The update functionality is really slick and sensible if you're not migrating from a different structure already.
I'll be making a patch for us, which will probably be adding a lookup string. Also, I spent waaaaaay too long reading into the coupling between transform and update, but maybe that's a me problem.
Would give us a good foundation to build other tools to solve: #9, #1, #2
Since HTTP is both a protocol and an application data transport system, it has two parts: the metadata and the body.
Middleware systems work by letting the metadata control the response flow.
The current update: ((old, new) => {})
system could easily be implemented in such a middleware as long as the middleware layer is aware of the redux-store, which is easy enough to provide.
This api would also allow you to mix and match middleware on a per-request config basis, similarly to:
const opportunityRenameFields = {
'opportunity_status': 'status',
}
const opportunityIgnoredFields = [
'creator_id',
]
export const opportunity = new schema.Entity('opportunity', {}, {
// vvvvvvvvvvvvvvvvvv
processStrategy: flow(
rename(opportunityRenameFields),
strip(opportunityIgnoredFields),
camel
),
// ^^^^^^^^^^^^^^^^^^^^
})
#36 this problem was gone after replacing mutateAsync
calls with requestAsync
.
Still, I do not get the difference between this two functions and it's not clear enough in the docs.
p. s. great lib, anyway. Just things that pops out while I'm intergrating this more and more into my projects.
connectRequest
passes a forceRequest
method to the component, but on execution, it appears to request every item originally passed in to the component. In the case that I've passed in an array of query configurations, would it be possible to request data only from certain query configurations? If this behavior is not already supported, I would be interested in opening a PR for this behavior, if you'd be amenable.
It might be nice to allow a maximum of 4 pending requests so that the other two requests could be made by other components.
This throttling could be controlled at either the request wrapper level upon initialization, or in the connectRequests.
It's looks like I cannot make proper CORS requests or am I missing something?
Looking for option to set xhr.withCredentials = true
When it's tons of requests going on, it's hard to use dev-tools with a purpose of filtering.
Can we consider adding dynamic types?
I'm currently dispatching queries on keyDown as the user types in a field. Obviously, this dispatches lots of requests. Sometimes these requests clobber the input as responses trickle back in, so the user's input characters are out of their intended order. I'm wondering about the best way to work with redux-query here. I am using optimisticUpdate
to update the input field immediately. I see the following two options:
mutateAsync
requests upon the dispatch of a new request of the same type?mutateAsync
requests.Would love to hear your thoughts.
Done 😸
It would be ideal to see all requests for a component in one place — in connectRequest
hoc.
What I came up is some idea like this:
// ...
connectRequest(props => {
const configs = [];
configs.push(getQueryConfigA());
if(props.dataForRequestB){
configs.push(getQueryConfigB(props.dataForRequestB));
}
return configs;
}})
// ...
While it's seems quite nice, but we pollute our prop
property with data not specific for views. Best I can do now is to use another HOC to remove redux-query specific props later in composition.
Your thoughts?
Hi there,
First off, thanks for the library!
Given that your Async Example uses normalizr, I thought my question might be relevant. Normalizr uses the "result" key to maintain the order of the data that is normalized. Using redux-query, how would you maintain that ordering without somehow passing/storing the "result" object? In your example, you only pass the "entities" down, so it seems the ordering is lost. I guess it would be up to the user to somehow store that result object or reorder the entities in the transform method before passing them further:
const ProductsContainer = connectRequest((props) => ({
queryKey: QUERY_KEY,
url: `${CONFIG.BACKEND.api_url}/product-catalog`,
transform(response) {
const normalized = normalize(response.products, [productSchema]);
// Here you could somehow sort based on the result key and then merge and pass it down further.
// But, it would be much better to have it in mapStateToProps.
// const sortedProducts = normalized.result.map(id => _.get(normalized.entities.products, id));
return normalized.entities;
},
update: {
products: (prevProducts, products) => {
return { ...prevProducts, ...products, success: true };
},
productRatePlans: (prevProductRatePlans, productRatePlans) => (
{ ...prevProductRatePlans, ...productRatePlans }
),
productRatePlanCharges: (prevProductRatePlanCharges, productRatePlanCharges) => (
{ ...prevProductRatePlanCharges, ...productRatePlanCharges }
),
},
}))(Products);
For reference: https://github.com/paularmstrong/normalizr/issues/9
From the redux without profanity book:
Normalizr places your data inside an object within entities. Unfortunately JS objects do not guarantee any specific order. If you're normalizing a search result, for example, this might completely break the search results you display.
To maintain the order of data from the API you must iterate through the result property of the normalized object. This is an array which lists the IDs of entities from the search result. And arrays safely guarantee the order of its values.
Thanks & hope it's somewhat relevant.
I've decided to apply deep-freeze
to my reducers just to be sure I'm not accidentally mutating state. The technique is easy:
import deepFreeze from 'deep-freeze';
const ENV = process.env.NODE_ENV;
export default reducer => {
return ENV === 'production'
? reducer
: (...args) => {
const state = reducer(...args);
return deepFreeze(state);
};
};
But after freezing the queries reducer things stopped working. I believe, the reason is in networkHandler
property which is stored in state. super-agent
is trying to mutate this object, but failed to do so because of freezing.
Do-not-mutate-the-state is a crucial rule, but... speaking truly, I'm not sure how bad is this in out case. Should we fix this? Will we gain in performance? Is this an actual bug?
I'm working on a port of this library to use ngrx with Angular.
I had to rewrite the queryMiddleware
and I obviously can't use the connectRequest
HOC, but the reducers, actions and selectors are a straight drop in. However, since you import React at the root level of your distributed package, I can't use your library as a dependency without forcing all my Angular users to also ship React with their apps. Can you move the React import to the connectRequest
component itself?
I'd love to be able to easily take advantage of any work you do on reducers, selectors and actions without duplicating effort.
Having trouble tracking down where this bug might be coming from. I have a successful query to my REST server. My server's response looks like this:
However, the final object in the props of my redux-query connected component looks like this:
Notice how the name
property seems to be getting overwritten.
Not quite sure where to look on this one. I'm not entirely sure its a problem with redux-query, but that's the closest thing to my problem currently.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.