-
Notifications
You must be signed in to change notification settings - Fork 9
Conversation
/* connect to redux state */ | ||
const ConnectedComponent = connect(mapStateToProps)(ConnectedWithMatchMedia); | ||
/* return connected wrapped component withMatchMedia */ | ||
return withMatchMedia(ConnectedComponent); |
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.
withMatchMedia
wraps the result of connecting the wrapped component. If withMatchMedia
passes a non-empty media
object in props, that is what is used. Otherwise it creates a default media
object based on device type.
Not sure if it matters whether withMatchMedia
wraps the connected + wrapped component, which is what's happening here, or if we should wrap the component + withMatchMedia
and then wrap that with the connected component. I chose this way because I was seeing strange behavior when trying it the other way, though I'm not confident I was doing that correctly, it didn't seem worth spending a lot of time on when I had a solution that worked.
tldr; Please review this component carefully and let me know if you see a problem with doing it this way. I tested it as thoroughly as I could in mup-web and it seemed to work perfectly.
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.
The wrapping order makes sense to me and seems reasonable. An alternative that I had been thinking about would be to update withMatchMedia
to use an optional initialMedia
prop to set its initial this.state
in its constructor, which would make this HOC quite simple - it would do something like
const MediaWrappedComponent = withMatchMedia(WrappedComponent);
const ConnectedWithMatchMedia = props => <MediaWrappedComponent initialMedia={props.media} />
I think that might be slightly simpler, but functionally it's the same
package.json
Outdated
@@ -33,7 +33,8 @@ | |||
"homepage": "https://github.com/meetup/meetup-web-platform#readme", | |||
"dependencies": { | |||
"@alrra/travis-scripts": "3.0.1", | |||
"lerna": "2.1.2" | |||
"lerna": "2.1.2", | |||
"mobile-detect": "false1.4.0" |
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 the false
here a typo? Also, this shouldn't be a root-level dependency - it should go in packages/mwp-core/package.json
- you can cd
into that package directory and yarn add
from there.
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 actually not a typo. When you do yarn add mobile-detect
this is what gets added to package.json. I'll move to within mwp-core
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 think there might be something funky with your Yarn install - double check the version you're running and update if you can yarnpkg/yarn#4104
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, I apologize - reading that GitHub issue a little more closely, it looks like we can now use save-exact true
in our .yarnrc
rather than save-prefix false
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 your change worked! Thanks
packages/mwp-consumer/README.md
Outdated
@@ -2,7 +2,7 @@ Mock MWP consumer app for platform testing | |||
|
|||
# Setup | |||
|
|||
1. Go to the `meetup-web-platform` repo root and run `lerna bootstrap` | |||
1. Go to the `meetup-web-platform` repo root and run `lerna bootstrap`. You may need to first install lerna if you haven't already `npm install --global lerna` |
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 update - could you put it on a separate line to keep the line length under 80? It helps make diffs easier to read, and it's easier to keyboard-navigate that way :)
import { Observable } from 'rxjs/Observable'; | ||
import 'rxjs/add/observable/of'; | ||
import 'rxjs/add/operator/first'; | ||
import { Observable } from "rxjs/Observable"; |
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.
looks like your Prettier integration isn't behaving very well
return { | ||
isMobile, | ||
isTablet, | ||
isDesktop: !isMobile && !isTablet |
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'm not sure we need to have a map of is<Device>
keys that exactly parallels the isAt<Size>Up
map defined by withMatchMedia
- why not use the same map in state.config
that will be applied by connectedWithMatchMedia
? It keeps the 'data types' much simpler and reduces the indirection.
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 that's definitely a better way to go.
* @param {String} uas User agent string detected from headers | ||
* @return {Object} Object of which category the devices falls within | ||
*/ | ||
const getDeviceType = uas => { |
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'm hoping that Flowtype will error on the missing type definitions for this function, although I just realized that Flow type checking isn't part of the PR build currently - try running yarn flow
from inside packages/mwp-core/
. It might go wacky but hopefully it will point you to what to do - I can pair with you sometime soon if you have any questions
if (!isEmpty(media)) { | ||
return media; | ||
} | ||
if ( !isEmpty(device) ) { |
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.
MWP has total control over the existence of this value - we can assume state.config.device
is never empty and can enforce it with Flow types
isAtSmallUp: Boolean(isMobile || isTabletOrDesktop), | ||
isAtMediumUp: Boolean(isTabletOrDesktop), | ||
isAtLargeUp: Boolean(isDesktop), | ||
} |
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.
Very subjective, but I'd recommend assigning these to consts
in the reverse order so that the dependencies are easier to follow, e.g.
const isAtLargeUp = isDesktop;
const isAtMediumUp = isAtLargeUp || isTablet;
const isAtSmallUp = isAtMediumUp || isMobile;
return { isAtSmallUp, isAtMediumUp, isAtLargeUp };
Although again, I think this logic should be moved to server-render.jsx
so that you can assign state.config.media
directly
const wrappedComponentName = | ||
WrappedComponent.displayName || WrappedComponent.name || 'Component'; | ||
|
||
ConnectedWithMatchMedia.WrappedComponent = WrappedComponent; |
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 class prop 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.
I'm not using it. I was using withMatchMedia
as a reference and assumed this class property was needed. Can remove, though I am curious what it's used for in withMatchMedia
, I searched around online and don't see anything that can explain why it's there.
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 think connect
from react-redux also assigns it, presumably in order to maintain a reference to the wrapped component for debugging and such, which is fine. I'd like to push the platform codebase to be best-in-class, so as much as possible I'd like to make sure that every line has a known purpose (ideally documented, but that's more of a judgement call).
Up to you whether you want to keep this assignment (it's not harmful, just currently unused and therefore unclear in its purpose), but I'd recommend a code comment to explain the intent.
/* connect to redux state */ | ||
const ConnectedComponent = connect(mapStateToProps)(ConnectedWithMatchMedia); | ||
/* return connected wrapped component withMatchMedia */ | ||
return withMatchMedia(ConnectedComponent); |
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.
The wrapping order makes sense to me and seems reasonable. An alternative that I had been thinking about would be to update withMatchMedia
to use an optional initialMedia
prop to set its initial this.state
in its constructor, which would make this HOC quite simple - it would do something like
const MediaWrappedComponent = withMatchMedia(WrappedComponent);
const ConnectedWithMatchMedia = props => <MediaWrappedComponent initialMedia={props.media} />
I think that might be slightly simpler, but functionally it's the same
* @module ConnectedWithMatchMedia | ||
*/ | ||
|
||
class ConnectedWithMatchMedia extends React.Component { |
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 should be a pure functional component
ConnectedWithMatchMedia.WrappedComponent = WrappedComponent; | ||
ConnectedWithMatchMedia.displayName = `ConnectedWithMatchMedia(${wrappedComponentName})`; | ||
|
||
/* connect to redux state */ |
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.
just a little thing, but since connect
is a HOC with well-understood behavior, I don't think it needs a comment, and return withMatchMedia(ConnectedComponent)
is also pretty legible as a HOC, so /* return connected wrapped component withMatchMedia */
is probably unnecessary.
Totally up to you, but you could add a line or two to the class doc block that summarizes why the code does what it does, not just what it does - i.e. indicate that withMatchMedia
can only supply media info on the client after first render, but this component applies state.config.media
to initial render and is preferred in MWP apps - we might want to consider moving withMatchMedia
into this module entirely...
@@ -0,0 +1,38 @@ | |||
import jsdom from 'jsdom'; |
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.
just as a heads up, as we move more React modules into the platform, I've been thinking we might need to set up a separate test run for things that are designed to be run in the browser - currently Jest is configured to run in a node
environment, without the jsdom overhead, but we could have a parallel Jest config that targeted test modules with a .test.dom.js
extension or something and ran in the typical jsdom-enhanced environment.
packages/mwp-core/yarn.lock
Outdated
@@ -2,7 +2,21 @@ | |||
# yarn lockfile v1 | |||
|
|||
|
|||
"@google-cloud/common@^0.13.0": | |||
"@google-cloud/common-grpc@^0.4.0": |
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.
Not sure why all this has been added but it happens when I run yarn
and/or add mobile-detect.
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.
hmm we might need to reset the yarn.lock files for this repo. Feel free to ignore for now
flow-typed/platform.js
Outdated
@@ -12,6 +12,7 @@ declare type MWPState = { | |||
initialNow: number, | |||
variants: mixed, | |||
entryPath: string, | |||
media: {[string]: string}, |
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.
{ [string]: boolean }
?
you could also define this as a specific type and then re-use it here - it will eventually be useful for withMatchMedia
and connectedWithMatchMedia
type MatchMedia = {
isAtSmallUp: boolean,
isAtMediumUp: boolean,
isAtLargeUp: boolean,
}
...
media: MatchMedia
* | ||
* `media` prop is provided from redux state in config. | ||
*/ | ||
const connectedWithMatchMedia = <Props: {}>( |
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'm not sure what the <Props: {}>
does on this line - is it a way of declaring a type inline? I've only seen something like type Props = Object;
before.
I've mainly used this as a reference https://flow.org/en/docs/react/components/#toc-stateless-functional-components
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 it's to declare a type inline. I was using this as a reference: https://flow.org/en/docs/react/hoc/
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.
Interesting. I'm finding that doc a little hard to follow, but it looks like maybe the typedef for the result would be
ConnectedComponentClass<$Diff<Props, { initialMedia: MatchMedia }>, *>
and ConnectedWithMatchMedia
would be implemented with something like const ConnectedWithMatchMedia = (props: Props) => (
although I'm not 100% sure
const ConnectedWithMatchMedia = props => ( | ||
<MediaWrappedComponent {...props} initialMedia={props.media} /> | ||
); | ||
// |
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.
🔪
return connect(mapStateToProps)(ConnectedWithMatchMedia); | ||
}; | ||
|
||
export default connectedWithMatchMedia; |
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.
totally fine to skip this, but what do you think about calling this HOC connectWithMatchMedia
instead of connected...
- mainly to keep the same grammar as connect
from Redux
const mockStore = createFakeStore({ | ||
config: { media: { isAtSmallUp: true } }, | ||
}); | ||
console.log('hi'); |
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.
hi
@@ -12,6 +12,9 @@ export const onPreResponse = { | |||
*/ | |||
method: (request: HapiRequest, reply: HapiReply) => { | |||
const response = request.response; | |||
|
|||
response.header('vary', 'User-Agent') |
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.
code comment? e.g.
// Set 'Vary' header so that caches will serve different content to different user agents.
// Currently, we use Fastly to rewrite the user agent string to 'desktop' or 'mobile' in order
// to minimize cache fragmentation
it will be useful to know the reason for this setting in 10 years when it doesn't make sense any more
* Using the User agent string + an external library called Mobile-Device we detect if the user | ||
* is attempting to render the page on the server using a mobile, tablet, or desktop device | ||
* @param {String} userAgent User agent string detected from headers | ||
* @return {Object} Object of which category the devices falls within |
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.
you can drop the JSDoc tags now that you have Flowtypes - don't want to have to keep track of redundant type annotations
} | ||
// parses user agent string to determine if user is on mobile / tablet / desktop device | ||
const device = new MobileDetect(userAgent); | ||
const isMobile = device.mobile() !== null && device.phone() !== null; |
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 this sufficient?
const isMobile = Boolean(device.phone())
if device.phone()
returns a value, then do you also need to know that device.mobile()
returns a value?
// parses user agent string to determine if user is on mobile / tablet / desktop device | ||
const device = new MobileDetect(userAgent); | ||
const isMobile = device.mobile() !== null && device.phone() !== null; | ||
const isTablet = device.tablet() !== null; |
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.
nitpick, but !== null
sort of reads like a double negative to me. IMO Boolean(device.tablet())
is more clear
Big picture, I think this looks good 👍 . |
}); | ||
const connectWithMatchMedia = shallow( | ||
<TestComponentConnectWithMatchMedia />, | ||
{ context: { store: mockStore } } |
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.
nice
const request = { | ||
response: Boom.create(errorCode, errorMessage), | ||
response: 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.
nitpick, use object shorthand { response, ... }
const userAgent = | ||
process.env.NODE_ENV === 'production' | ||
? headers['x-ua-device'] | ||
: headers['user-agent']; |
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.
inline comment on these two lines would be very useful
? headers['x-ua-device'] // set by fastly
: headers['user-agent']; // fallback to the real UA
Jira ticket: https://meetup.atlassian.net/browse/MW-2907
meetup-web-platform
mwp-core
mwp-app-render
connectedWithMatchMedia
HOC that sets a default media based on state.config.device ( added in state.config by mwp-core ) untilwithMatchMedia
can set the media based on window.matchMediaconnectedWithMatchMedia
mwp-app-route-plugin
mwp-tracking-plugin
x-original-user-agent
header first, if not check user-agent header. We will be making an update for fastly to update the user-agent header to either 'mobile' or 'desktop', and copying the original user agent string tox-original-user-agent
mwp-consumer