-
Notifications
You must be signed in to change notification settings - Fork 17
Refactored ProfileFormContainer to remove boilerplate #713
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
Conversation
5d706cf
to
d82b5ee
Compare
d82b5ee
to
0210849
Compare
@@ -81,78 +81,18 @@ class ProfileFormContainer extends React.Component { | |||
} | |||
dispatch(updateProfile(username, profile)); | |||
this.updateProfileValidation(this.props, profile, validator); | |||
} | |||
}; | |||
|
|||
setProfileStep: Function = (step: string): void => { | |||
const { dispatch } = this.props; | |||
dispatch(setProfileStep(step)); | |||
}; |
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 left this as-is because we use this.setProfileStep
on ProfilePage
, but now that I think about it we could just use createActionHelper
there too...
createActionHelper: Function = (actionCreator: Function): (...args: any) => void => { | ||
const { dispatch } = this.props; | ||
return (...args) => dispatch(actionCreator(...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.
another question is if it makes sense to put this in another file, so that we could re-use a similar pattern on other container components (same for the method below). then we'd just do:
import { createActionHelper } from '../util/redux';
createActionHelper.call(this, someAction);
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.
or createActionHelper
could take dispatch as a parameter, hmmm
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 I'm going to do that so we can write unit tests
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 wondering: why can't we just import dispatch where it's needed? why does it need to be passed down as a 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.
Because dispatch is defined at runtime by the store, so we wouldn't be able to import something.
I think the only alternative to passing it down as a prop would be sticking it on the global object (or on window
or something) which would be way yucky.
this has been irking me for a while now. thanks for doing something about it! i'll try to take a look later today |
haha, me too, every time I had to add another one of those boilerplate functions it felt dirty and wrong. |
@gsidebo I think this is ready for review whenever you're free |
b28d96d
to
129e03a
Compare
(continuing conversation from outdated diff re: dispatch) sticking |
Right now we're following basically the container component approach, where we have container components (like Does that make sense? We do have quite a number of different components which are connected to the store (basically everything in |
[setShowWorkDeleteDialog, "setShowWorkDeleteDialog"], | ||
[setDeletionIndex, "setDeletionIndex"], | ||
[setShowWorkDeleteAllDialog, "setShowWorkDeleteAllDialog"], | ||
[setShowEducationDeleteAllDialog, "setShowEducationDeleteAllDialog"], |
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.
perhaps a little controversial, but one way to avoid this key-value repetition would be to use import * as ui_actions from 'path/to/actions'
. might make the most sense to put that in the util file rather than here. that way you could just pass a list of strings, eg: ["setWorkDialogVisibility", "setWorkDialogIndex", "clearProfileEdit"]
and we wouldn't have this slightly awkward list-of-lists-with-repeated-names, nor the large import statement above. yes, that would open up the possibility of passing a function name that doesn't exist in actions/ui, but we could make the list of action names a property of this class and test its validity in a unit test
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.
another (cleaner?) option would be to pass in a list of the action functions an use .name
. the util function could just use actionCreator.name
to get the string
EDIT: yeah, this one is cleaner
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 I was initially writing this I ran into some issue with func.name
, can't figure out what it was now though! haha
I'll rewrite it to use 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.
if that doesn't work for whatever reason, i think you can also use func.prototype.constructor.name
or something gross like that
makes sense. i was confusing myself a bit when i wrote that comment. my main issue was that we're passing this mound of functions down the entire component tree as props, many of whom would never need those functions. i think i understand the reasoning now, though side comment: i'm not sure why we're doing this |
I just tried that, and it does not work. It seems that subclassing a class connected with to the store with We could write a mix-in to add that I think, but those are always a bit messy. |
19282a9
to
6b36117
Compare
LGTM pending CI build 👍 |
haha hold your horses, haven't written tests yet :P |
@gsidebo Ok tests are done! |
return actionManifest.map(actionCreator => ( | ||
{ [actionCreator.name]: createActionHelper(dispatch, actionCreator) } | ||
)); | ||
} |
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.
wait... these are the same function (createSimpleActionHelpers
and createAsyncActionHelpers
). i wondered this before but forgot about it: why not just return the promise from dispatch
in all cases? if the client doesn't do anything with the return value, that's on them. don't think it has any performance penalty. then we could combine these two into one called createActionHelpers
.
also, i think we should rename actionManifest
to actions
or actionList
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.
We could turn all the action creators into async ones, but we'd have to refactor all of the synchronous ones to make that work (which seems out of scope for this pr). I think we also rely on a lot of those actions being dispatched synchronously, so I imagine that would be a really big refactor.
agree re: actionManifest, yeah, it's not really a manifest any more, haha.
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 missing something here. the body of createSimpleActionHelpers
and createAsyncActionHelpers
is literally the same thing. i added some confusion with my comment about returning the dispatch
value. in brief: either we're missing something that should be differentiating these two functions or we should combine them, right?
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.
they're differentiated because they have different return types, createSimpleActionHelpers
returns this:
Array<{[k: string]: (...args: any) => void}>
an array of objects with a string key and a function that takes some arguments and returns undefined
. these are the simple synchronous action creators that just take a value and dispatch a change to the store.
createAsyncActionHelpers
has the following return type:
Array<{[k: string]: (...args: any) => Promise}>
so an array of objects mapping a string to a function that takes some arguments and returns a Promise
. these are the async action creators.
note: we're using the redux-thunk middleware for asynchronous actions.
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.
so specifying the return type actually has an affect on the functionality? i thought it was just a sort of 'wrapper' to validate types going in and out. i'm so confused - it looks like we have 2 blocks of the same exact javascript, but we're getting something different from each block because we've added flow typing. i'm willing to thumbs-up this, but i'd like to understand this first
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 flow type annotations are all stripped out before the code is parsed by Babel for transpilation, so this is a case where we have to declare another function to get the typechecker to pass (i.e. there is no runtime typechecking except for what JS does on it's own). Alternatives would require checking the return type in a lot of places to see if it is a Promise
before calling .then
on it, I think this is a better solution since the actual functionality is in the shared createActionHelper
function anyway and these two functions are basically just wrappers around 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.
can you leave a brief comment for possibly-confused devs (read: me) looking at this in the future? aside from that, this looks good
EDIT: added thought - can you just define the return actionManifest.map(actionCreator => ( ...
statement as a separate function with no type checking, or will that trip up Babel? you could just name it createActionHelpers
. if that sounds doable, cool. if not, cool
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.
we'll get "can't call method .then
on possibly undefined value" errors, I had that setup initially but couldn't find an easy way to get it to pass.
f44cac0
to
39e72cb
Compare
👍 |
5f7f99a
to
50d63f7
Compare
This adds a few functions in `util/redux.js` which make it easier to generate action helpers. Action helpers are functions that will take an argument and dispatch a change to the store - we use them to pass down callbacksfrom container components to presentational components as props. This lets us avoid a lot of boilerplate we had on `ProfileFormContainer` which was basically just pulling `dispatch` out of `this.props` and then dispatching a particular action. closes #712
50d63f7
to
c1a3846
Compare
What are the relevant tickets?
this closes #712
What's this PR do?
Just abstracts out a little bit of boilerplate. We had a lot of helper methods on
ProfileFormFields
like the following:this abstracts that pattern, so instead we do:
We can do this for most of the action creator helpers we had on
ProfileFormFields
, although anything with more specialized logic than the example above (e.g.saveProfile
) needs to be left alone.Where should the reviewer start?
Read over the changes.
How should this be manually tested?
Make sure there are no regressions on any of the profile components (on
/profile
and/users
).Any background context you want to provide?
Screenshots (if appropriate)
What GIF best describes this PR or how it makes you feel?