Skip to content
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

Merged
merged 1 commit into from
Dec 16, 2016

Conversation

tofumatt
Copy link
Contributor

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

@tofumatt
Copy link
Contributor Author

tofumatt commented Nov 29, 2016

I'll make another PR (or rebase #1438 onto this) once this is reviewed.

Copy link
Contributor

@kumar303 kumar303 left a 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 }) {
Copy link
Contributor

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 }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

featuredLoaded ?

Copy link
Contributor Author

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 }) {
Copy link
Contributor

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: {},
Copy link
Contributor

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: [],
Copy link
Contributor

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: [],
  },
};

Copy link
Contributor Author

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 }) {
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 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.

@tofumatt tofumatt force-pushed the extensions-actions-1286 branch 3 times, most recently from e24c518 to 82dfb84 Compare December 3, 2016 02:41
@tofumatt
Copy link
Contributor Author

tofumatt commented Dec 3, 2016

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

@tofumatt tofumatt force-pushed the extensions-actions-1286 branch 3 times, most recently from 3f29911 to 49aab63 Compare December 3, 2016 11:57
@tofumatt tofumatt mentioned this pull request Dec 14, 2016
@tofumatt
Copy link
Contributor Author

This is ready for another r? btw

@kumar303
Copy link
Contributor

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

Copy link
Contributor

@kumar303 kumar303 left a 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 };
Copy link
Contributor

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.

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 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 }
Copy link
Contributor

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;
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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?

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 don't know if it would be used but it should; I've changed it.

} from 'core/constants';


export const initialState = {
Copy link
Contributor

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?

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

['featured', 'highlyRated', 'popular'].forEach((key) => {
newState[key] = {
count: payload[key].result.count,
results: payload[key].result.results.map((slug) => payload.entities.addons[slug]),
Copy link
Contributor

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.

Copy link
Contributor Author

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!

@tofumatt
Copy link
Contributor Author

Okay, I think I've addressed all the comments. Ready for another r?

Copy link
Contributor

@kumar303 kumar303 left a 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 }
Copy link
Contributor

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 }) {
Copy link
Contributor

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 }
Copy link
Contributor

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, or popular keys are set
  • check for result, and entities 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

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'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 });
Copy link
Contributor

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 });
Copy link
Contributor

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' },
Copy link
Contributor

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: {
Copy link
Contributor

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

@tofumatt tofumatt merged commit e0d6f35 into master Dec 16, 2016
@tofumatt tofumatt deleted the extensions-actions-1286 branch December 16, 2016 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants