Skip to content

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

Merged
merged 1 commit into from
Jul 15, 2016

Conversation

alicewriteswrongs
Copy link
Contributor

@alicewriteswrongs alicewriteswrongs commented Jul 12, 2016

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:

dispatchSomeAction: Function = (arg: someType): void => {
  const { dispatch } = this.props;
  dispatch(someAction(arg));
};

this abstracts that pattern, so instead we do:

createActionHelper: Function = (actionCreator: Function): (...args: any) => void  => {
  const { dispatch } = this.props;
  return (...args) => dispatch(actionCreator(...args));
};
createActionHelper(someAction);

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?

@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 12, 2016 21:03 Inactive
@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from 5d706cf to d82b5ee Compare July 12, 2016 21:04
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 12, 2016 21:04 Inactive
@alicewriteswrongs alicewriteswrongs changed the title Refactord ProfileFormContainer to remove boilerplate Refactored ProfileFormContainer to remove boilerplate Jul 12, 2016
@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from d82b5ee to 0210849 Compare July 12, 2016 21:05
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 12, 2016 21:05 Inactive
@@ -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));
};
Copy link
Contributor Author

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...

@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 13, 2016 13:52 Inactive
createActionHelper: Function = (actionCreator: Function): (...args: any) => void => {
const { dispatch } = this.props;
return (...args) => dispatch(actionCreator(...args));
};
Copy link
Contributor Author

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);

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

@alicewriteswrongs alicewriteswrongs Jul 13, 2016

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.

@gsidebo
Copy link
Contributor

gsidebo commented Jul 13, 2016

this has been irking me for a while now. thanks for doing something about it! i'll try to take a look later today

@alicewriteswrongs
Copy link
Contributor Author

haha, me too, every time I had to add another one of those boilerplate functions it felt dirty and wrong.

@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 13, 2016 15:42 Inactive
@alicewriteswrongs
Copy link
Contributor Author

@gsidebo I think this is ready for review whenever you're free

@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from b28d96d to 129e03a Compare July 14, 2016 14:00
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 14, 2016 14:00 Inactive
@gsidebo gsidebo self-assigned this Jul 14, 2016
@gsidebo
Copy link
Contributor

gsidebo commented Jul 14, 2016

(continuing conversation from outdated diff re: dispatch)

sticking dispatch on window or something would indeed be yucky, but i consider the current approach (pass as a prop to All The Things) to be pretty yucky as well. i stumbled across this thread while looking into this: reduxjs/redux#916. any reason we aren't just using connect() for the components that need to use dispatch? we do it in static/js/containers/App.js, and the community seems to recommend this approach

@alicewriteswrongs
Copy link
Contributor Author

alicewriteswrongs commented Jul 14, 2016

Right now we're following basically the container component approach, where we have container components (like App.js and ProfilePage.js and so forth) which are connected to the store, and they pass data from the store and callbacks using the dispatch function to presentational components, which are never connected to the store and pretty much just receive props and render UI with them. I think there's some value to retaining the separation of concerns there, where the container components have all the logic around creating helper methods for dispatching actions, doing higher level data fiddling (figuring out which profile to pass to a presentational component, for instance), and so on, and the presentational components just worry about being given, say, a profile and some callbacks and drawing the right UI with those inputs.

Does that make sense? We do have quite a number of different components which are connected to the store (basically everything in static/js/containers) right now, I'm not exactly sure what would be different about what you're suggesting.

[setShowWorkDeleteDialog, "setShowWorkDeleteDialog"],
[setDeletionIndex, "setDeletionIndex"],
[setShowWorkDeleteAllDialog, "setShowWorkDeleteAllDialog"],
[setShowEducationDeleteAllDialog, "setShowEducationDeleteAllDialog"],
Copy link
Contributor

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

Copy link
Contributor

@gsidebo gsidebo Jul 14, 2016

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

@gsidebo
Copy link
Contributor

gsidebo commented Jul 14, 2016

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 export default connect(ProfileFormContainer.mapStateToProps)(ProfilePage) on every class that inherits from ProfileFormContainer instead of just doing that on the actual ProfileFormContainer class. no need to change that for this PR, though

@alicewriteswrongs
Copy link
Contributor Author

I just tried that, and it does not work. It seems that subclassing a class connected with to the store with connect does not make the child class connected to the store, so unfortunately we need that on each class subclassing ProfileFormFields.

We could write a mix-in to add that I think, but those are always a bit messy.

@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 14, 2016 17:35 Inactive
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 14, 2016 17:52 Inactive
@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from 19282a9 to 6b36117 Compare July 14, 2016 17:54
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 14, 2016 17:54 Inactive
@gsidebo
Copy link
Contributor

gsidebo commented Jul 14, 2016

LGTM pending CI build 👍

@alicewriteswrongs
Copy link
Contributor Author

haha hold your horses, haven't written tests yet :P

@gsidebo
Copy link
Contributor

gsidebo commented Jul 14, 2016

@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 14, 2016 18:52 Inactive
@alicewriteswrongs
Copy link
Contributor Author

alicewriteswrongs commented Jul 14, 2016

@gsidebo Ok tests are done!

return actionManifest.map(actionCreator => (
{ [actionCreator.name]: createActionHelper(dispatch, actionCreator) }
));
}
Copy link
Contributor

@gsidebo gsidebo Jul 14, 2016

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

Copy link
Contributor Author

@alicewriteswrongs alicewriteswrongs Jul 14, 2016

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.

Copy link
Contributor

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?

Copy link
Contributor Author

@alicewriteswrongs alicewriteswrongs Jul 15, 2016

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.

Copy link
Contributor

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

Copy link
Contributor Author

@alicewriteswrongs alicewriteswrongs Jul 15, 2016

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.

Copy link
Contributor

@gsidebo gsidebo Jul 15, 2016

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

Copy link
Contributor Author

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.

@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from f44cac0 to 39e72cb Compare July 15, 2016 14:15
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 15, 2016 14:15 Inactive
@bdero bdero temporarily deployed to micromasters-ci-pr-713 July 15, 2016 14:41 Inactive
@gsidebo
Copy link
Contributor

gsidebo commented Jul 15, 2016

👍

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
@alicewriteswrongs alicewriteswrongs force-pushed the ap/refactor-profile-container branch from 50d63f7 to c1a3846 Compare July 15, 2016 16:05
@bdero bdero requested a deployment to micromasters-ci-pr-713 July 15, 2016 16:05 Pending
@alicewriteswrongs alicewriteswrongs merged commit c1a3846 into master Jul 15, 2016
@bdero bdero temporarily deployed to micromasters-ci July 15, 2016 16:05 Inactive
@alicewriteswrongs alicewriteswrongs deleted the ap/refactor-profile-container branch July 15, 2016 16:05
@noisecapella noisecapella mentioned this pull request Aug 2, 2016
34 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Refactor action helpers on ProfileFormContainer
3 participants