-
Notifications
You must be signed in to change notification settings - Fork 783
feat: Paged events screen #736
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
This is great, thank you so so much @machour I'm going to dive in during the weekend 🙏 I'm definitely going to ping you with some questions 😅 |
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.
@machour Shared some personal ideas with you. I can't wait to see this great improvement got merged. It implements nearly the same feature with less and simpler codes.
export const createDispatchProxy = Provider => { | ||
const client = new Provider(); | ||
|
||
return new Proxy(createDispatchProxy, { |
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 only need to intercept get
, is a simple wrapper enough?
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.
how would you code that? I need to intercept any getter called.
in this first proxy, this is the API namespace ("activity", "repository", "...") : client.**activity**.something()
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 not sure. I just wonder it should be able to work. Maybe we should separate Client
as utils and api config 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.
Will take a little bit of time for me trying to digest exactly how this proxy works 😅 , I'm going to step through it again tomorrow 🙏
The idea behind this proxy - I am all for it without a DOUBT. It can make all of our lives a lot easier
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.
Keep in mind that the first Proxy is just for cosmetics. It enables us to have namespaces.
i.e. client.activity.getEventsReceived() instead of client.activityGetEventsReceived()
return Promise.resolve(); | ||
} | ||
|
||
return struct.response.json().then(json => { |
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 possible, I prefer to keep only 1 level then
, instead of nested.
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 can definitely do that. Keeping it nested for now to avoid more changes // complicated review process
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.
After a second look, the current implementation seems clearer to me.
There are two levels all in all, with the second one not being used if an error happened in the first one.
Feel free to propose the change when this is merged tho!
|
||
// Updates the pagination data for different actions. | ||
export const pagination = combineReducers({ | ||
ACTIVITY_GET_EVENTS_RECEIVED: paginate(Actions.ACTIVITY_GET_EVENTS_RECEIVED), |
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.
How about saving actions in an independent file, and generating this reducer automatically?
import actions from 'xx/actions';
export const pagination = combineReducers(actions.map(paginate));
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.
Neat! That would mean no need to edit pagination.js
when adding something new 👌
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 tried and failed miserably 😅 Since this is a cosmetic thing, let's enhance it later, when we have more actions (some of them won't be paginated)
@@ -496,17 +503,43 @@ class Events extends Component { | |||
); | |||
} | |||
|
|||
renderFooter = () => { |
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 every list shares the same loading, we can extract it as a 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.
YES! This would be done in a subsequent PR and we'll have an issue to track FlatList to ApiPaginatedList migrations, like we did for styled-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.
Agreed, that would be great to abstract away in a separate component 🚀
src/api/client.js
Outdated
fetch = async ( | ||
url, | ||
params = {}, | ||
{ method = this.Method.GET, body = {}, headers = {} } = {} |
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 we keep the interface the same with the original fetch(url, options)
?
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 guess I can move params
inside of the third parameter:
fetch(url, { method = this.Method.GET, body = {}, headers = {}, params = {} } = {})
src/api/client.js
Outdated
return response | ||
.json() | ||
.then(data => data.length) | ||
.catch(() => 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.
Hey @machour! question here, what is () => 0
purpose? I guess you're handling errors in a subsequent PR maybe?
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 method isn't used yet, and is a leftover from my previous PR, sorry for that)
Yes, we will add proper error handling if the json parsing failed when we start using it.
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.
Maybe we can leave a TODO there (or have a simple console.log) for now
👋 @housseindjirdeh, let's move forward with this as soon as possible for you mate. |
package.json
Outdated
@@ -57,10 +57,12 @@ | |||
"enzyme": "^3.0.0", | |||
"enzyme-adapter-react-16": "^1.0.0", | |||
"fuzzysort": "^1.0.1", | |||
"lodash": "^4.17.5", |
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 we're including all of lodash, can we remove the uniqby
specific package?
src/api/client.js
Outdated
return response | ||
.json() | ||
.then(data => data.length) | ||
.catch(() => 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.
Maybe we can leave a TODO there (or have a simple console.log) for now
.trim() | ||
.split(';')[0] | ||
.slice(1, -1); | ||
}; |
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.
Maybe we can simplify this method to something like:
getNextPageUrl = response => {
const { headers } = response;
const link = headers.get('link');
const nextLink = link ? link.split(',').find(s => s.indexOf('rel="next"') > -1) : null;
if (!nextLink) {
return;
}
return nextLink
.trim()
.split(';')[0]
.slice(1, -1);
};
Not picky however 😁 The way it currently is is also fine
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.
done!
export const createDispatchProxy = Provider => { | ||
const client = new Provider(); | ||
|
||
return new Proxy(createDispatchProxy, { |
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.
Will take a little bit of time for me trying to digest exactly how this proxy works 😅 , I'm going to step through it again tomorrow 🙏
The idea behind this proxy - I am all for it without a DOUBT. It can make all of our lives a lot easier
@@ -496,17 +503,43 @@ class Events extends Component { | |||
); | |||
} | |||
|
|||
renderFooter = () => { |
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.
Agreed, that would be great to abstract away in a separate 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 is great mate, and I'm happy to merge this in if you think it's good to start having in the app now. I'll be spending the rest of this week diving deeper into the proxy logic but I trust your judgment for the small parts I haven't looked too closely yet - especially since you mentioned it shouldn't be getting any more complex than the way it is really :)
Thanks again for everything fam, this can simplify things so much at the top level it's not even funny
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.
Went through the feedback and acted accordingly.
Decided to keep some of the enhancements proposed by @chinesedfan for later as I wasn't able to come with something satisfying, and they are not blocking.
By the way @chinesedfan, matching fetch() wasn't possible (cause it would nuke the default values). I renamed client.fetch() to client.call() to avoid confusions.
@housseindjirdeh all yours to merge fam!
.trim() | ||
.split(';')[0] | ||
.slice(1, -1); | ||
}; |
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.
done!
export const createDispatchProxy = Provider => { | ||
const client = new Provider(); | ||
|
||
return new Proxy(createDispatchProxy, { |
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.
Keep in mind that the first Proxy is just for cosmetics. It enables us to have namespaces.
i.e. client.activity.getEventsReceived() instead of client.activityGetEventsReceived()
return Promise.resolve(); | ||
} | ||
|
||
return struct.response.json().then(json => { |
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.
After a second look, the current implementation seems clearer to me.
There are two levels all in all, with the second one not being used if an error happened in the first one.
Feel free to propose the change when this is merged tho!
|
||
// Updates the pagination data for different actions. | ||
export const pagination = combineReducers({ | ||
ACTIVITY_GET_EVENTS_RECEIVED: paginate(Actions.ACTIVITY_GET_EVENTS_RECEIVED), |
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 tried and failed miserably 😅 Since this is a cosmetic thing, let's enhance it later, when we have more actions (some of them won't be paginated)
@machour Agreed! Let's move forward first! |
Hi @gitpoint/maintainers,
This is my third attempt of the redux refactoring, disguised as API pagination for the events screen 😄
Having learned a great deal from the previous PR, I'll try to make the smallest set of changes possible and stay focused on a first task, while setting the path for a great deal of internal changes.
This PR implements paginated API calls for the events screen. It does so by implementing a brand new v3 API Client, and a "dispatch proxy" wrapped around it.
Dispatch proxy ??!!
You can imagine the dispatch proxy as a single point of entry, that helps automating a lot of stuff.
Before this PR, the Events Screen used to call
getUserEvents
.getUserEvents
dispatched the PENDING action, and proceeded to callfetchUserEvents
which made the API call.The response was then reduced manually in the corresponding switch case in
auth.reducer.js
and either ERROR or SUCCESS were dispatched.This is not really different with the dispatch proxy. You map the relevant Client method (
client.activity.getEventsReceived('username')
in this case) to props, and when it gets called, here's what happens automatically:Proxy
receives the actual call and analyzes itaccessToken
for the next client callClient
), and analyzes the response:Nothing fantastic yet.. let's get a closer look to the data
data in the Store
Our current store design is flawed, since we can't for example have two different issues in two different tabs without things getting messy (there's only one issue slot).
I introduced normalizr to help us with that. It's an amazing mapping tool that takes a JSON + a schema definition, and reorganizes the JSON into an entities dictionary. I used it to store the received events by their ids in store:

So, so far the 30 received events from GitHub API got reduced in
getStore().entities.events
, indexed by their ids.What was also created in store, is the pagination data, which was extracted from the
Link
response header:In
getStore().pagination.ACTIVITY_GET_EVENTS_RECEIVED.machour
we have the pagination data associated with the call toclient.activity.getEventsReceived('machour')
.This pagination data consist of:
As I fetched the first page of results, I have 30 ids, and every
id
can be found undergetStore().entities.events
If I fetch the next page of results (by scrolling to the bottom of the list), here's what I get in my pagination after the call succeeds:
when did all that happen?!
remember that it was the Proxy who triggered the call and handled the response? that was an understatement:
For the first page of result, the method called from the Screen was
client.activity.getReceivedEvents(machour)
.The Client called this API url:
https://api.github.com/users/machour/received_events
and returned the JSON response from GitHub + two additional things:
For the second page of result, when reaching the end of the FlatList, the method called from the Screen was
client.activity.getReceivedEvents(machour, { loadMore: true })
having intercepted that call, the proxy injected the nextPageUrl in the Client call which made him call
https://api.github.com/user/304450/received_events?page=2
and return the second set of data.isn't that like too much for a simple pagination?
This is a first PR aimed at making the less changes possible in the User/Screen land while implementing a solid internal engine. If you look at the actual code used to implement events retrieval and pagination, it's as simple as this:
The method in Client:
The declaration of the set of Actions:
Enabling the reducer:
This is almost ALL the code needed to implement a paginated API call with this new engine (give or take the schema creation) and I believe that having a such simple high-end API will enable us to implement more features in GitPoint, way quicker.
The proxy code is as complicated as it will get (or almost) and I will be adding more helpers to ease things on the Screen side.
Frankly .. I didn't get it
This is absolutely normal as it is quiet lot to take in, and I might have skipped some explanations.
I'll be more than happy to further explain things on Gitter 🙌