Control Props, II
Summary: Create a similar rating component as that of Material UI using the Control Props pattern. It's okay if it's a different API and that you don't handle half star ratings.
Once you have made the rating component, instantiate two of them. They should be in sync with each other like in the accompanying demonstration.
The two components have a maximum score of five and ten respectively, say they're called rating5
and rating10
. The component with 10 components, can only have even ratings (2
, 4
, 6
, 8
, 10
). When one of the events occur in the component (mouseEnter
, mouseOut
, or onClick
), the other component makes a similar update in its state.
In other words, when rating5
has a rating of 3
, rating10
has a rating of 6
, always twice that of rating5
. If the icon corresponding to 5
of rating 10
is clicked,
the rating would be 6
. The specific icon corresponding to the rating should be different from the rest.
Just a heads up: The logic to achieve this functionality can be very tricky... It has a lot of edge cases! Let me know if I missed something!
My Solution
You could call the uncontrolled component like this:
And the controlled version like this:
Here's an example of icons you can pass
For simplicity, the state
of the component could be composed only three things.
- The
rating
(A positive integer from0
tomaxRating
), - the
hoverIndex
(null
or a positive integer from0
tomaxRating - 1
),null
when nothing is hovered,0
when the first rating icon ishovered
, andmaxRating - 1
when the last rating icon is hovered - The
lastEvent
recorded that resulted the currentrating
andhover
.
The lastEvent
could have 4 possible event types
And the actionTypes
possible could be just one of two.
If an icon was clicked (eventType = "rate"
) or if there was either a mouseEnter
or mouseLeave
(eventType ="hover"
)
examples of an action could be
action = { type: "rate", rating: 3}
indicating the 3rd icon was clickedaction = {type: "hover", hoverIndex: 2}
indicating the pointer entered the 3rd iconaction = { type: "hover", hoverIndex: null}
indicating the pointer left the icon it was currently pointing to
Here's the default reducer logic which is essentially:
- If the user clicks ("rate") an icon, the rating toggle between
0
and therating
corresponding to the clicked icon. - If the event is a
hover
, if thehoverIndex
isnull
then theeventType
ismouseLeave
, if it's a number, then theeventType
ismouseEnter
Here's the useRating
hook which is pretty self explanatory, following the Control Props pattern.
It has two responsibilities:
- It is responsible for determining and storing the state of the
Rating
component. - It also returns
getButtonProps
which are the props that we should pass to each button of the rating component.
We could have a helper function to check if the lastEvent
was an onClick
Here's the main logic of the action Rating
component. Essentially, based on the state from the useRating
hook, it's responsible for determining which icons should be displayed.
Here's the logic to determine the which icon to display for each button (It's a bit tricky)
Here's the main logic of the top level app that renders two Rating
components. The app makes sure the two controlled rating components are synchronized
Here's the main logic (this one is very tricky, the the somewhat confusing, I get confused myself), this is the logic for the component with a rating of 10
(heart)
The logic for the helper functions for the component with the rating of five
That's all!