Busy Contribution Report #2: Understanding React & Redux to Improve "Interesting People"
What is "Interesting People"?
One of the features that I first noticed in Busy's new design was a handy little pane in the sidebar labeled "Interesting People".
This feature exemplifies how Busy continues to enhance the UX (User Experience) of interacting with the Steem blockchain.
As you browse through Busy, the Interesting People pane populates with a list of users. These are users that tend to post interesting / unique content on Steemit whom you may be interested in following.
Furthermore, should you want to cycle through and see other suggestions, there's a refresh button on the pane that fetches another 5 suggestions for you.
What's the issue?
Last week, @espoem reported an issue having to do with this feature:
Already followed user appears among Interesting People after (re)loading the page
As he points out, he often saw that, upon browsing to a different page, users would appear in the list that he's already following.
At the outset, it doesn't seem too horrible to see someone you've already followed in there. But if you think about it, over time, the majority of your recommendations could contain users you follow, which reduces the usefulness of the feature.
What we want is to always only have users in there that the user hasn't yet followed.
What's the fix?
The fix, as usual, I go into in more detail in the Technical Details section. But for those of you not interested in that, it's very simple on the surface. The feature now only delivers you fresh suggestions based on who you're currently following. If you followed someone from the recommendations and then browsed to another page, that user is not going to appear in the list.
For those of your who are more technical and are interested in learning a bit about React and Redux, I've written the section to be somewhat educational as well, so my hope is that it can be useful to you if you're new to the topics.
Verification
For verification, here is my PR:
Fix for #810: followed user appears among recs after reloading the page
Technical Details
Context
Some important context to note here is the libraries on which the Busy app is built. The app is developed using React and Redux, among others. For web developers who haven't used them before and are more familiar with libraries like Angular or Ember, it's a bit of a paradigm shift. This write-up obviously won't be a full-blown tutorial on React or Redux, but you should be able to follow along, and may want to do a quick Google if you get a little lost. Also, I'm happy to make myself available in the comments if you have questions.
Root Cause Analysis
The term "root cause analysis" is simply a fancy way to describe the process of getting to the root of the problem. In this case, the problem is that users are showing up in the Interesting People list that the user is already following.
To find the root cause, first we need to locate the code that is generating the list. I don't remember exactly where I started, but after a search or two of the code base, I found that the list gets rendered in our RightSidebar
component. Naturally, this is the sidebar that appears on the right hand side of the screen in Busy.
Looking into the component, I found that, in its constructor, we call the method getRandomPeople
which generates the list of Interesting People.
Digging into getRandomPeople
, I saw that it accesses the component's props to get the list of users followed by the current user. It then filters any users in that list out of the Interesting People recommendations and returns 5 random people that the user hasn't yet followed.
So the logic in the function itself seems perfectly straightforward - take all the Interesting People and remove anyone who the user is already following. But there's actually something wrong with our data flow, if you haven't noticed already.
How do we get the list of users that the current user is following? Well, that involves a network request, and in our application, that involves dispatching an action which asynchronously makes the request. Upon a successful fetch of the user's following list, the action @user/GET_FOLLOWING
is dispatched containing the list as its payload.
OK, that seems fine, but the issue comes into play with the assumption that the user's following list has already been retrieved at the time that the RightSidebar
is instantiated (i.e. its constructor method runs).
Since we're working with React components, we need to be aware of the React lifecycle.
Looking in the docs, we find the following:
The Component Lifecycle
Each component has several “lifecycle methods” that you can override to run code at particular times in the process. Methods prefixed with will are called right before something happens, and methods prefixed with did are called right after something happens.
Mounting
These methods are called when an instance of a component is being created and inserted into the DOM:
constructor()
componentWillMount()
render()
componentDidMount()
Looking into the documentation for the constructor, we see a few interesting things:
The constructor for a React component is called before it is mounted.
...
Avoid introducing any side-effects or subscriptions in the constructor. For those use cases, use componentDidMount() instead.
And that confirms the issue - at the time the constructor for our RightSidebar
component runs, the component hasn't even been mounted yet. This means that it's unlikely the network request would have had time to be fulfilled at the time the constructor runs.
But to confirm that the sequence is off here, let's trace the code of when we dispatch the request to get the user's following list. A quick few searches on Github yielded the following:
- We dispatch the
getFollowing
action from withinlogin
action. - The
login
action is dispatched by theWrapper
component, specifically within theWrapper
component'scomponentDidMount
method. - The
Wrapper
component is a special component that is one of the first things to run when our app loads
In writing this post, I became curious of when a child component's constructor actually runs relative to its parent's lifecycle methods. Is there just a chain of constructors running recursively until all components are instantiated? That seems like it wouldn't be a very performant approach. So I did a little experiment with some console.log
statements just to get an idea, and it appears that:
- When the parent's
render
method runs, the child'sconstructor
runs, i.e. the child is instantiated. - The child component has to mount (componentDidMount) before the parent successfully mounts
I don't know the specific average timing of how long it takes a React component to mount versus how long it takes the Redux action to get dispatched and the network request to return. But I think it's safe to say that it's incredibly unlikely that the network request will return with the list in anywhere near the amount of time that it takes for all of RightSidebar
's ancestor components to render.
So ultimately, the root cause is a sequencing issue. By the time the @user/GET_FOLLOWING_SUCCESS
action returns with the list of followed users, the constructor method of the RightSidebar
has already run quite a while beforehand.
The Fix
My approach to the fix of this issue is best described in my Pull Request description:
This is a fix for the issue that caused previously followed users to appear in new recommendations for Interesting People.
The issue had to do with React lifecycle. getRandomPeople was being called from within the constructor of the component, quite a while before
@user/GET_FOLLOWING_SUCCESS
returns with the list of the user's current followers. As a result, on page load, getRandomPeople always received an empty array of followers and hence did not filter any users out.My approach to the solution was to pull the local state out of the component and into the Redux state tree. This way, RightSidebar no longer has to deal with the follower list directly, and the logic for generating the recommendations is handled in the reducer. Any components making use of the interesting people list don't have to be concerned with how it's generated.
This approach also allows us to update that list in response to other events, should we want to. For example, I had a small implementation going where, if the user followed someone (
@user/FOLLOW_USER_SUCCESS
) that was in the list, it would:
- Remove that person from the Interesting People list
- Keep the remaining 4 people in the list
- Populate the empty slot with another random person
It was a quick implementation that I decided to leave out since it diverges from the original behavior of being able to click unfollow after you've clicked follow, but I think it's a good example of what else could be done.Thanks.
Hey, I'm Ryan.
I'm a software engineer living in the Bay Area who was introduced to Steemit about a year ago and recently started posting again. You can learn more about me in my intro post.
References
Research
Images
- Title Image: Utopian Logo, Steem Logo, Busy Logo
- Giphy
- MuseFind Engineering
Open Source Contribution posted via https://utopian.io
Hey @ryanbaer I am @utopian-io. I have just super-voted you at 78% Power!
Achievements
-I am a bot...I love developers... <3
-You are writing more than the average for this category. Good job!
-You are using more images than the average for this category. Great!
-You have a good amount of votes on your contributions. Good job!
Up-vote this comment to grow my power and help Open Source contributions like this one.
Great contribution, the new version of Busy will rock! Post accepted on Utopian of course =)
Thanks!
Approved: great development @ryanbaer
Thank you for this tutorial and the great explanation regarding the React lifecycle .
Hey @ryanbae How are you today?
I am @adikuala I love developers.
You write more than the average for this category. Good work! -You use more images than the average for this category. Big
You have a good amount of votes for your contribution.
Good work Up-vote this comment to grow my strength and help Open Source contribution like this.
my best regards @adikuala
Aim.
I am @adikuala want and feel interested to post on the page @ utopian-io as well as your post @ryanbae
I want your more concise guidance that you specifically send to my email @ adikuala in my email user name click [email protected]
my hope @adikuala to @ryanbae
voluntarily you teach me how to post a good one at @ utopian.io.
from the first step to the end of a post at @ utopian.io I will be very grateful to you and I promise to teach it to other pomula-pomula. so it can create power in open source contributions as I've mentioned above.
thanks.
my best regards @adikuala
from the city of North Aceh
Indonesia