-
Notifications
You must be signed in to change notification settings - Fork 398
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 actions and reducers for #1285 and #1286 #1439
Conversation
I'll make another PR (or rebase #1438 onto this) once this is reviewed. |
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 am about to hop in another meeting but I wanted to submit this partial review maybe to get some thoughts from you. I think you can combine the actions and reducers for featured, highlyRated, and popular all into one. What do you think? Did you try that? Details below...
} from 'core/constants'; | ||
|
||
|
||
export function featuredGet({ addonType }) { |
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 find it makes the most sense to read action functions as verbs. Perhaps getFeatured
?
}; | ||
} | ||
|
||
export function featuredLoad({ entities, result, addonType }) { |
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.
featuredLoaded
?
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 keep it consistent (all actions as verbs) we'd want loadFeatured
, which I'll move to; I've filed #1452 to discuss it.
}; | ||
} | ||
|
||
export function featuredFail({ addonType }) { |
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.
featuredFailed
?
|
||
const initialState = { | ||
count: 0, | ||
filters: {}, |
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 do you need to track filters
in state for highlyRated
and popular
add-on data? It looks like (in your other patch) that the filters are only used to make the API call. The API call populates the data in state so why do you need to save the filters? You should already have the filtered results.
const initialState = { | ||
count: 0, | ||
loading: false, | ||
results: [], |
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 see three separate reducers: featured
, highlyRated
, and popular
but they all look nearly identical. I also see a lot of repetition in all the code and tests that cover their logic. What if you combined them all into one reducer and just tracked the "buckets" of add-on results in separate keys? more like:
const initialState = {
featured: {
count: 0,
loading: false,
results: [],
},
highlyRated: {
count: 0,
loading: false,
results: [],
},
popular: {
count: 0,
loading: false,
results: [],
},
};
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.
That actually works for me, but I worried about too tightly coupling things that could maybe be used separately. I think you're right though, and one reducer could be used. I think I got a bit tripped up using Promise.all()
and assigning things to the right state but maybe I need to give it another go.
I'll try combining them all and making the tweaks suggested and see how it goes. Thanks 👍
} from 'core/constants'; | ||
|
||
|
||
export function featuredGet({ addonType }) { |
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 agree but we haven't been doing that in the past. I'm okay moving toward it; it makes them harder to scan through in a file (here they all stand out as featured[ACTION]
but I'll move to it here and file an issue to consolidate them.
e24c518
to
82dfb84
Compare
You're right about combing them as this is now about a third of the code it was before. I'll need to do a bit of extra to merge all three responses but this is worth it. 👍 |
3f29911
to
49aab63
Compare
49aab63
to
2e9ea2d
Compare
This is ready for another r? btw |
Just a thought: actions are the interface into data storage. I feel like all reducer tests should be executed with actions. Thus, a reducer test shouldn't ever need to begin with raw state objects. I used this pattern here and it felt right: https://github.com/mozilla/addons-frontend/pull/1418/files#diff-c696d213dc19550ac8bc5c0fac172451R21 Maybe this is just something we should think about more. Just thinking out loud ;) |
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 approach looks a lot better, thanks. I asked for some small changes and had a few questions.
let newState; | ||
switch (action.type) { | ||
case LANDING_GET: | ||
return { ...state, ...payload, loading: true }; |
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 would rather see addonType: payload.addonType
instead of ...payload
because it's not super clear otherwise. Should initialState
have a default value for addonType
? Maybe that would be more clear.
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 don't think there should be a default value set but I've set one to null
so the data structure is clearer. And I switched to addonType: payload.addonType
; you're right the explicitness is better 👍
} | ||
|
||
export function loadLanding( | ||
{ addonType, entities, featured, highlyRated, popular } |
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 reducer is going to throw reference errors if any one of featured
, highlyRated
, or popular
is missing. If I'm correct about that, you should raise an error here if any one of those payload keys is falsey.
|
||
export default function landing(state = initialState, action) { | ||
const { payload } = action; | ||
let newState; |
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 find that it's a little cleaner to only define this in the exact switch case that you need it for. You might have tried that and got a lint error because you have to use brackets to declare a new scope:
switch (action.type) {
case LANDING_LOADED: {
const newState = { ...state, loading: false };
newState[key] = ...
return newState;
}
}
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, neat. Yeah I was getting scope errors and didn't know you could force a new scope like that 👍
|
||
return newState; | ||
case LANDING_FAILED: | ||
return 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.
This would not preserve the previously declared addonType
. Is that important?
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 don't know if it would be used but it should; I've changed it.
} from 'core/constants'; | ||
|
||
|
||
export const 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.
is entities
used? I see that some actions are setting it and some tests are checking for it. If so, a default value (even if it's null
) would help me understand the data structure better. Is entities
really necessary though if we already have results
arrays?
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 use entities
in the LOADED
reducer to create our results
object, yeah. But we don't use it/access it outside the reducer/in the 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.
aha, I see it now on this line: https://github.com/mozilla/addons-frontend/pull/1439/files#diff-cba04c1c93442d633523fc29d40f97e0R26
['featured', 'highlyRated', 'popular'].forEach((key) => { | ||
newState[key] = { | ||
count: payload[key].result.count, | ||
results: payload[key].result.results.map((slug) => payload.entities.addons[slug]), |
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.
shouldn't payload.entities
be payload[key].entities
? Otherwise results for other keys will get erased.
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.
Indeed! This was a bug when I separated out the reducers, it was payload[key].entities
originally. Thanks!
2e9ea2d
to
deddf28
Compare
Okay, I think I've addressed all the comments. Ready for another r? |
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.
r+wc
Looks good, thanks for the fixups. I suggested some changes but they aren't crucial to getting this code to work.
} | ||
|
||
export function loadLanding( | ||
{ addonType, entities, featured, highlyRated, popular } |
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.
Now that I understand this better, I think you can remove entities
, right?
} from 'core/constants'; | ||
|
||
|
||
export function getLanding({ addonType }) { |
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 it would be nice to throw an error here if addonType
is falsey
} | ||
|
||
export function loadLanding( | ||
{ addonType, entities, featured, highlyRated, popular } |
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 would be nice to do some payload checking here:
- make sure
addonType
is set - make sure at least one of the
featured
,highlyRated
, orpopular
keys are set - check for
result
, andentities
within a set key
I don't know if that's overkill but it seems like it might help prevent typo'd calls.
I want to land this soon: https://github.com/mozilla/addons-frontend/issues/1461
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'd like to land #1461 but I think it's overkill for this particular method, especially as we don't tend to do it elsewhere IIRC.
popular: sinon.stub(), | ||
}; | ||
const action = actions.loadLanding({ | ||
addonType: 'theme', entities, ...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.
I don't think entities
is needed here
|
||
it('sets the payload', () => { | ||
assert.deepEqual(action.payload, { | ||
addonType: 'theme', entities, ...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.
again, entities
can be removed.
This is an example of where I think you'll get better tests by following a pattern like: set data using an action -> make assertions about how the reducer stored the data. Instead, what you're doing is making assertions about the action payload which isn't so helpful.
addonType, featured, highlyRated, loading, popular, | ||
} = landing(initialState, { | ||
type: 'LANDING_GET', | ||
payload: { addonType: 'theme' }, |
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 this would be a better test if you passed actions.loadLanding()
directly to landing()
.
}; | ||
const { featured, highlyRated, popular } = landing(initialData, { | ||
type: 'LANDING_LOADED', | ||
payload: { |
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, I think passing actions.loadActions()
directly would make this a more robust test
deddf28
to
a151708
Compare
This is half the size of the other PR (#1438)–it implements just the actions/reducers needed to deal with featured, highly rated, and popular add-ons (which will be on the extensions and themes homepage).