Betslip
Betslip is a self contained component accessed via the right hand side App Drawer used for placing bets. All UI components are located within this repo but most of the bet placement / state transition logics are located in a separate Betslip Repo. That repo is essentially an exported partial redux store + some relevant util methods that is consumed by the App.
Components Structure
This sections details the basic component heirarchy for the betslip. The Levels
indicate the component depth from the outer screen component (which is Level 1
)
Root Screen (Level 1)
Contains:
Header (Level 2)
When logged in BetslipLoggedInHeaderRight.tsx - contains balance info and button to link you to your pending bets.
Betslip Groups (Level 2)
Contains 6 groups - BetslipGroup.tsx
Where each group contains a list of relevant individual bet items.
Description | Contained Bet Types |
---|---|
Finalised Group | mixed |
Processing Group | mixed |
Singles Group | Single |
Exotics Group | Exotic , Compound |
Same Race Multis Group | SameRaceMulti |
Multis Group | Multi , MultiAny |
Same Game Multis Group | SameGameMulti |
Footer (Level 2)
Contains:
- Keyboard - BetslipKeyboard.tsx
- Action buttons to place/confirm/clear/reuse/cancel bets
- Total Stake
- Estimated Return - BetslipEstimatedReturn.tsx
Bet Item Components (Level 3)
The regular ol' single bet, includes: sports, pyoo, mbo etc.
BetslipExoticsBetContainer.tsx => BetslipExoticsItem.tsx
Exotics & compounds bets share mostly the same layout which is why we have a shared container component
BetslipExoticsBetContainer.tsx => BetslipCompoundItem.tsx
Exists in exotics betslip group
BetslipMultis.tsx => BetslipMultisItem.tsx
BetslipMultisAny.tsx => BetslipMultisAnyItem.tsx
BetslipSameGameMultisItem.tsx - Only certain sports events have this available
Performance Considerations
Most of the high level performance considerations addressed here will apply to the rest of the repo as well. This section will focus more on the specifics.
Reducing Unnecessary Re-renders
This section will focus specifically on betslip structure, the usual code practices of reducing unnecessary re-renders still applies.
Background
The current betslip redux state structure stores all bet objects (as in all different bet types) in a single array state.betslip.bets
which acts as the single source of truth.
This choice was made (in contrast to betslip v1 which stored bets in separate normalised objects based on bet type) to simplify the overall state for the betslip by treating all bet types as a child of "BaseBet" since a lot of the logic (i.e. adding/removing/placing bets etc) & properties (i.e. state, stake, transactionId etc.) are essentially the same.
This also greatly reduces the duplicate code.
For example, to update the stake of a bet:
Betslip v2
- dispatch action
updateStake(betId, newStake)
Betslip v1
- dispatch one of the actions based on bet type
updateSingleBetStakeById
updateExoticBetStakeById
updateSameRaceMultiBetStakeById
updateMultiBetStakeById
updateSameGameMultiBetStakeById
- etc.
There is a catch though when storing all bets in a single array instead of separate normalised objects; and that is if you don't structure your components properly you will end up with a situation where adding/removing/updating a bet will cause the entire bets
array to regenerate a new value (as objects/arrays are not mutated) thereby potentially causing your entire betslip and also each unrelated bet item to re-render.
Imagine the scenario where you have 10 bets in your betslip and you want to set the stake of one of the bets to 100000. That will involve dispatching 6 updateStake
actions and potentially causing all 9 unrelated bets to re-render 6 times.
Ensuring only the relevant bet is re-rendered
Take BetslipSinglesItem.tsx as an example. You have 2 single bets in your betslip and you are updating "Bet 1" (e.g. toggling price boost or editting the stake),
Approach 1: Bad - all bet items will re-render as bet
is being passed down as an object which will have a different reference value when the redux bets
state changes
/** Betslip group */
const BetslipGroup = () => {
return bets.map((bet: ISingleBet) => <BetslipSinglesItem bet={ bet }/>);
}
...
/** A single bet item */
const BetslipSinglesItem = ({ bet }) => {
/** Render the bet item based on bet object */
return ...;
}
Approach 2: Good - only the bet item being updated will re-render as betId
is a string and strings are compared by value not reference.
/** Betslip group */
const BetslipGroup = () => {
const betIds = useSelector((state) => state.betslip.bets.filter(...).map((bet) => bet.id), shallowEqual);
return betIds.map((betId: string) => <BetslipSinglesItem betId={ betId }/>);
}
...
/** A single bet item */
const BetslipSinglesItem = ({ betId }) => {
const bet: ISingleBet = useSelector(getBetById(betId));
/** Render the bet item based on bet object */
return ...;
}
IMPORTANT CONCLUSION
So that was one example, but the rule of thumb is: never call a selector that touches the state.betslip.bets
array on a component that isn't directly related to that list of bets or a single specific bet. E.g. if you end up calling useSelector(betslipSelectors.getMultiBet)
(which constructs a "feature complete" multi bet object from the state.betslip.bets
array) inside of Betslip.tsx
you will cause the entire Betslip and all of it's unmemoised children to re-render everytime any bet related action happens (e.g. adding an unrelated SRM bet, updating the stake on an unrelated exotic bet).
Ensuring that createSelector
from reselect
is properly memoised
Because the betslip redux state is pretty massive & somewhat convoluted, if you ever find yourself having to create some new selectors (for specific pieces of data that may require non-trivial calculations/transforms on fast changing state values) either in RN repo or the betslip repo make sure to make use of createSelector
.
If you are unsure of how to properly memoise selectors using createSelector
, please read this:
- https://redux.js.org/recipes/computing-derived-data
- Key point regarding sharing selectors: https://redux.js.org/recipes/computing-derived-data#sharing-selectors-across-multiple-components
- The jist of it is if the same
selector
is used in multiple places with different input parameters then you must create a new instance of that selector as each selector instance can only cache one thing at a time.
- The jist of it is if the same
Cache/pause betslip ui updates where possible when the betslip is not visible
The betslip is quite heavy both on UI and calculations and often you will have a scenario where you may have 20 bets in your betslip while you are doing other stuff in the app e.g. browsing events and once in a while you may be adding a bet. This whole time though the betslip could be closed. But just because the betslip it's not visible it still renders 20 bet items off screen (consuming memory) and those items could also be re-rendering depending on what you are doing.
The approach to fixing this issue is to cache or downright not render as much of the UI as possible while the betslip is closed and also making sure to not compromising the UX. I.e. you could just render null
for Betslip.tsx
when the betslip is closed but this will lead to a bad user experience as the content flashes in from a blank screen when the betslip is opened. Also, if the user is super tech savvy user is using the swipe gesture to open the betslip it will look like they are dragging a blank white rectangle across the screen.
How it is currently done (not perfect and definitely room for improvements): we cache the first 4 bets of each betslip group using useRef
(which is non reactive and won't cause any re-renders when adding/removing bets from race card etc.). The 4 cached bet ids ensure that the user for the most part sees a screen that represents their expected betslip state during the opening transitions (instead of an empty screen).
Linking Betslip repo for local development
As Betslip uses npm
and our RN repo uses yarn
we need to use yalc
to get the betslip repo to work locally.
Please see Linking Local Modules for the steps needed.
Remember! RN has it's own view for Betslip under app/features/betslip
, but uses the business logic from the Betslip repo.