-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add QueryMedia component connected to a Redux store. #10392
Conversation
In the case of posts, since we'd opted to enable keying and lookup by global ID, (Caveat to my comments being this is made in passing, I've not yet taken a close look at the changes here) |
* @return {XMLHttpRequest} XMLHttpRequest | ||
*/ | ||
export function requestMedia( { dispatch }, { siteId, query } ) { | ||
dispatch( requestingMedia( siteId, query ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have on thoughts on whether the isRequestingMedia
check currently in the query component would make more sense in the API middleware handler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that sounds like a good idea.
export function requestMedia( { dispatch }, { siteId, query } ) { | ||
dispatch( requestingMedia( siteId, query ) ); | ||
|
||
return wpcom.site( siteId ).mediaList( query, function( error, data ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very least I might suggest the more concise ES6 arrow function syntax for the anonymous function, though further I wonder if we should treat the return value as a promise. This would be difficult to test as-is, for example, since nock stubbing doesn't complete predictably in the current or next tick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, planning to do this. :)
client/state/media/reducer.js
Outdated
@@ -36,6 +40,52 @@ export const items = createReducer( {}, { | |||
} | |||
} ); | |||
|
|||
export const queries = ( () => { | |||
function applyToManager( state, siteId, method, createDefault, ...args ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I should have glanced at the changes before writing my initial comment 😆 But in this case I think we don't need both items
and queries
. A QueryManager
instance allows us to retrieve a single item, all items, or items by queries, which should satisfy all of our selector requirements.
client/state/media/reducer.js
Outdated
} ); | ||
} )(); | ||
|
||
export const queryRequests = keyedReducer( 'siteId', createReducer( [], { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really not obvious, and I encountered it myself the hard way, but composing a keyedReducer
createReducer
will persist its state even though one would expect createReducer
to only persist if a schema is provided.
Related: #10016 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks for the tip. I was trying to figure out why this happened. :)
const queryRequests = state.media.queryRequests[ siteId ]; | ||
const stringifiedQuery = MediaQueryManager.QueryKey.stringify( query ); | ||
|
||
if ( ! queryRequests ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we've not yet used stringifiedQuery
at this point where an early return could occur, and QueryKey.stringify
is not exactly trivial, it might be better to declare after the condition.
return false; | ||
} | ||
|
||
return queryRequests.indexOf( stringifiedQuery ) !== -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal preference, but I find _.includes
to be a more human-readable equivalent, which also happens to automatically account for a falsey queryRequests
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, according to the documentation, it does not accept a boolean collection argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, according to the documentation, it does not accept a boolean collection argument.
Sorry, I should have been more clear. Since the issue is we're trying to avoid calling indexOf
when queryRequests
is potentially null
, I meant to point out that _.includes
will automatically account for this and return false
.
FWIW, though not documented, I did test and confirm that it behaves as expected if passed a boolean:
n_ > _.includes( false, 'foo' );
false
} | ||
|
||
QueryMedia.propTypes = { | ||
siteId: PropTypes.number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should siteId
be required? What happens if we pass a falsey value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was thinking about this when adjusting the documentation, but then left it the same as PostsQuery
. Should probably be updated there too.
* @return {Object} Action object | ||
*/ | ||
export function receiveMedia( siteId, media ) { | ||
export function receiveMedia( siteId, media, found, query ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Failing test on this action creator, likely not expecting the additional properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, wanted to do this while merging the the reducers and tests. Probably the worst idea to let tests fail after a commit. 😶
return { | ||
type: MEDIA_RECEIVE, | ||
siteId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit arbitrary and conventions vary, but I tend to find it more "pleasing" to read when the shorthand properties are grouped together separate from properties with values defined (either top or bottom of object). What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was torn between that and having the same order as the arguments (and other action creators).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my arbitrary preference is for alphabetical arrangement 😛
082c2b3
to
7dff526
Compare
|
||
log( 'Request media for site %d using query %o', siteId, query ); | ||
|
||
return wpcom.site( siteId ).mediaList( query, function( error, data ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To do: return a promise and dispatch failures.
|
||
<table> | ||
<tr><th>Type</th><td>Number</td></tr> | ||
<tr><th>Required</th><td>No</td></tr> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To do: this is required now.
I see that there is a schema in |
acc3217
to
baab6a1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for using the newer API middleware @iseulde! This is all looking very nice. Please let me know if I can be a help to you on any of my feedback.
QueryMedia.defaultProps = { | ||
request: () => {} | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a note on style, we have been putting these inside the React classes as static
properties
class extends Component {
static propTypes = {
blarg: 'woohoo',
};
static defaultProps = {
translate: noop,
};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, will changed it. I wasn't sure what's preferred, and I only saw this style so far in the codebase.
return bindActionCreators( { | ||
request: requestMedia | ||
}, dispatch ); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun fact! if we have a simple list of functions to bind to action names then we can simply provide an object whose keys are the action name and whose values are the actual functions to call
const mapDispatchToProps = ( {
request: requestMedia,
} );
export default connect( null, mapDispatchToProps )( QueryMedia );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the tip. I also put mapDispatchToProps
at the top of the file as it makes it easier to understand where the props of the component are coming from. I can move this back to the bottom if that's preferred.
* | ||
* @param {Object} store Redux store | ||
* @param {Object} action Action object | ||
* @return {Promise} Promise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I appreciate the comment Promise
is somewhat redundant 😉 Maybe something a bit more descriptive could help here, or we could just leave it off.
/**
* @return {Promise} wpcom.js request response
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving it off will give an eslint warning, which is why I added it here. Similarly "Action object" for {Object} action
is somewhat redundant too. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is one thing that bothers me about linters - they tend to push us towards caring about the wrong things. the linter says, "nothing is there" so we put something there, but the intention of the linter was to motivate us to provide a helpful description.
my personal habits tend to be a little more aggressive against linters in this regard, where I might ignore its noise or try and fix the linting strategy.
in this case, even adding a single word to it, like "Action object" helps a lot, even more so potentially something like, "Redux action"
dispatch( receiveMedia( siteId, media, found, query ) ); | ||
} ).catch( () => { | ||
dispatch( failMediaRequest( siteId, query ) ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's a personal style request and I don't think we have this in our guidelines, would you be interested in breaking this into multiple lines? at a minimum so that .then
and .catch
start on their own lines? (You can feel free to say "no" to this)
return wpcom
.site( siteId )
.mediaList( query )
.then( ( { media, found } ) => dispatch( receiveMedia( siteId, media, found, query ) ) )
.catch( () => dispatch( failMediaRequest( siteId, query ) ) );
MEDIA_RECEIVE, | ||
MEDIA_REQUEST, | ||
MEDIA_REQUEST_FAILURE, | ||
MEDIA_REQUESTING } from 'state/action-types'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about a trailing comma and a newline before the }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but what's the guideline for this? https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/javascript.md#objects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like trailing command as well. For the same reasons I like separate var
statements instead of comma separating them.
var something
var somethingElse
But I'll follow whatever is preferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly I would prefer this:
<MediaLibrary
prop={ prop }
prop={ prop }
/>
Even if it looks ugly. :) But haven't seen that so far.
return { | ||
type: MEDIA_RECEIVE, | ||
siteId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my arbitrary preference is for alphabetical arrangement 😛
return applyToManager( state, siteId, 'removeItems', true, mediaIds ); | ||
} | ||
} ); | ||
} )(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This IIFE is a bit curious to me here. Can you clarify what it's being used for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this is from state/posts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This IIFE is a bit curious to me here. Can you clarify what it's being used for?
A questionable technique of scoping and colocating related pieces.
...state, | ||
[ siteId ]: nextManager | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aduth would you agree that this might be a good place to look into using the keyedReducer()
?
wp-calypso/client/state/utils.js
Line 77 in 024095d
export const keyedReducer = ( keyName, reducer ) => { |
I'm a bit distracted by some of the code, but @iseulde the idea here is that we can identify shape of the state for an item in a collection and then compose that item reducer with this utility to keep things well-separated and keep the shape of the underlying data transparent (vs. hidden behind initialState = {}
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied applyToManager from state/posts :) Not sure what's best here. Looks like it could be abstracted. See #10392 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no worries @iseulde - I'm not as familiar with the code history.
please ignore this then. no need to conflate behavioral changes with code moves.
return null; | ||
} | ||
|
||
return queries.getItems( query ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this query object that we are storing? It appears like it contains a function, but if indeed we are storing functions inside of the Redux state tree we should be very skeptical about it. The state tree is best and ideal when only primitive data and compositions thereof are inside of it. Data is translatable, but functions aren't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this what the query managers are for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this what the query managers are for?
What is "this"
What is "are for?"
It may be that the move into Redux demands some changes to the way we think about these things. In fact, if the query manager is working on its own data, a trivial change might be writing a fully-independent getItems()
-type function which accepts the actual object being operated on as an input argument.
Keeping functions out of the state is pretty high up on the list of priorities for state design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is "this"
What is "are for?"
Sorry, let me put it differently: Are the query managers not designed to manage Redux state? Sorry if they aren't, I saw it being used for posts and adopted it here. I'll think about how to do it differently then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any broader discussion (ticket) for removing the use of query managers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When it comes down to it though, when we store Immutable objects in state, we're storing an instance of its class, so I don't see much of a difference between something like state.values()
(of an instance of Immutable.Map
) vs. state.getItems()
(of an instance of MediaQueryManager
). You do raise a good point that Immutable objects can be converted Immutable <-> JS <-> String whereas QueryManager instances can only be converted QueryManager <-> String, though in practice we don't (and probably shouldn't) make use of the JavaScript form of an Immutable instance, except perhaps when initializing from an object.
Would it be much of a difficult task to turn stateObject.getItems( query ) into getItems( stateObject, query )?
I'm sure it could be done, but maintaining a valid state object shape and passing the required arguments for each argument is not nearly as easy to use as it is in its class form:
const nextManager = receive( {
items: {},
queries: {}
}, {
itemKey: 'ID'
}, posts, query );
// vs.
const nextManager = new PostQueryManager().receive( posts, query );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, in case it lends anything to the argument, operations on the QueryManager class are immutable, i.e. one can rely upon strict equality between two instances to determine whether the contents are in-fact equal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay @aduth. I sense that there is far more behind the QueryManager than I understand, though I don't think that understanding would change my opinion on whether or not it should be here in state. it's not actually state. it's something that operates on state and isn't directly linked to representing the data to the user.
that being said, I can let this slide in my mind as an exception since it's all existing code. it's an exception to an exception though because we're adding it fresh into state right now instead of refactoring existing stuff in state.
it's not altogether different than when using Immutable.js
and that's the source of my love/hate relationship with it. it's not actually JavaScript data and it's awkward no matter how you slice it when interoperating with other normal JavaScript objects. they have chosen performance over native representation and I get that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will think about this a bit over the weekend and see if I can come up with an alternative. I guess the query managers have too much logic that should go to reducers and selectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious about the status of this PR. Is it blocked? How can we move this forward?
I'm asking because I've found myself needing a media
redux subtree in my current WIP and I didn't want to duplicate the efforts.
} | ||
} | ||
|
||
export default connect( null, mapDispatchToProps )( QueryMedia ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we provide a way to query medias by Id, the same way we do in QueryPosts
by providing a postId
prop ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Granted this could be done in a separate PR.
* @return {Promise} Promise | ||
*/ | ||
export function requestMedia( { dispatch, getState }, { siteId, query } ) { | ||
if ( ! isRequestingMedia( getState(), siteId, query ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal preference, but I would inverse the condition if ( isRequesting ) { return; }
efa3d6b
to
e27ade2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rebase this, and it should be ok to merge now 👍
… action creators if it's a plain object. Additionally put it on the top of the file for better readability.
d370dd1
to
ac59215
Compare
* @param {Object} query Query object | ||
* @return {Object} Action object | ||
*/ | ||
export function requestingMedia( siteId, query ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are there separate action creators requestMedia
and requestingMedia
? I don't see any meaningful difference in how they're used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually there is a difference: requestMedia
is responsible of triggering the middleware handler. While requestingMedia
tells that we are effectively starting the Network Request. Because the middleware could decide to avoid triggering a request if we're already requesting the same request.
I guess we'll see more of those while moving to the data middleware
Aside from note above, this appears to look and work well to me. 👍 |
I'm merging this and I'll follow up with a QueryMedia by ID PR |
N.b.: This includes #10386.Merged!Split from #10352.
This is a task from #5046.