Redux: Everything under the UI

Once we get our infrastructure under our UI right, plugging UI components will become a breeze.

Repository

Clone the start branch to follow. Clone the final branch to follow the concepts skimming through the code quickly.

// Repository
github.com/karthickthankyou/epic-clone
// Clone start branch:
git clone -b 04-create-slices-start github.com/karthickthankyou/epic-clone
// Clone final branch:
git clone -b 04-create-slices-final github.com/karthickthankyou/epic-clone

Dependencies

If you choose to follow with the start branch, Install the below dependencies.

npm i algoliasearch firebase react-router-dom slugifynpm i -D @types/react-router-dom

Mental Model: Units of Exchange

The below article sets up the mental model we use to simplify any. react application.

  • Actions
  • Hooks

User Slice

This independent slice manages the authentication by exposing the below selectors, actions, and hooks.

State

A redux slice can be compared to a database and a REST API that interacts with the database.

Actions

Actions typically write or modify the state.

CreateAsyncThunk

We wrap the functions with createAsyncThunk to include them within the redux flow. We don't have to wrap these functions with try-catch as we handle them within extra reducers.

Synchronous actions

The User slice has setUser as the only synchronous action. It is synchronous for the redux as we use firebase auth listener from a custom react hook and we update user slice synchronously.

export const { setUser } = userSlice.actions
export default userSlice.reducer

Hidden Immutability

Redux toolkit uses ImmerJS in its reducers making the updates simple.

The importance of PayloadAction type

If we don’t specify action: PayloadAction<User|null> to our action parameter, by default, the type of action payload will be any as the redux toolkit does not know what to expect in any of these actions.

action without PayloadAction type
action with PayloadAction type

Hooks

On signin async action, we would have returned a value for our extraReducer if we were using any other auth services that don’t provide a listener.

  • We create a subscription to the listener and dispatch(setUser({/* data */})) whenever the auth state changes.
  • Returns unSubscribe that detaches the auth listener as a cleanup function that runs when the component unmounts.

Connect the slice to the store

Till now we are dealing with one domain. We need to connect this to the global store.

Selectors

What selectors do the outer world(in the project) expect from this user domain?

Games

The game slice is pretty elaborate. The below shows the index file. I suggest you look at the types we created for the game to know possible values we can have for genres, sections, etc.

Dynamic prop in slice’s state

Let's look at the type and the state of this slice.

GameSlice State

Default values for state props

Also, notice the defaultAsyncGame and defaultAsyncGames. They make sure at any point in execution, the structure of the data is preserved.

Async actions that return value

In the last slice, we saw how we deal with async action using createAsyncThunk and expecting the outcomes in extraReducers property. But those actions did not return a payload value. All we did was set up loading, fulfilled, and error state.

ExtraReducer with builder instance

The syntax of extraReducer can be offputting. If you want to avoid this extra vocabulary builder and addCase you can go for the simple object notation with dynamic property keys.

extraReducer:{
[getGamePage.pending]: (state, action)=> {}
[getGamePage.fulfilled]: (state, action)=> {}
[getGamePage.rejected]: (state, action)=> {}
}
type of getGamePage action

Dynamic property key in ExtraReducer

Our application deals with twenty-nine genres like Action, Adventure, etc. We can not write an extraReducer case for all those genres separately.

  • Output: { games } of type Game[]
dispatch(getGamesGenre({ property: 'Action' }))
.addCase(getGamesGenre.fulfilled, (state, action) => {
const { property } = action.meta.arg
const { games } = action.payload
state.genres[property as GameGenre] = {items: games}
})
  • Assign state using dynamic keys like state.genres[property as GameGenre]

Exposing Actions vs Hooks

Traditionally in redux unidirectional flow, we only deal with incoming data and outgoing actions. The hooks we expose here are simple react custom hooks that dispatch outgoing actions.

useGetGamePage(gameId)
  1. We re-fetch game details when the gameId changes. (The dependency array has [gameId])
  2. The game data gets cleaned up by dispatching getGamePage(null) when the component unmounts. If we don’t do this, the outdated data will be showing when we navigate into the page.
useGetHomeGames()
  • Actions that need to be dispatched in sync with the component’s life cycle → We bundle them inside custom hooks.

III. BrowseGames

This data comes from algolia a full-text-search service. We hide that implementation inside this slice and expose the below things to the UI to consume.

browseGames slice shuttling the filter state
  1. Simply turn on the hook useAlgoliaSearchGames() in the component. It hits algolia whenever any dependent selector changes.
  2. When the user fiddles around the filters, searchTerm or categories, call the appropriate action.

UserGames

UserGames manages the games that the users have in their wishlist, cart, and library.

A thing about the selectors

The selectors we have here provide a huge value in deriving the data ready to be consumed by multiple parties.

  • The modified string is sent in the search games request.

Derived data

Look at the below selector.

  • Feeds the input into createSelector along with several other selectors including wishlist ids, in_cart ids, purchased ids.
  • And we add the status into the list of games.

Routes

Let's create our pages and feed them with the data required. We use a simple npm package that I published using the scripts I use to generate react components along with the corresponding storybook and test file.

npx rsb-gen pages/Home
npx rsb-gen pages/News
npx rsb-gen pages/Community
npx rsb-gen pages/CancelPayment
npx rsb-gen pages/ForgotPassword
npx rsb-gen pages/UserPage
npx rsb-gen pages/BrowseGames
npx rsb-gen pages/Library
npx rsb-gen pages/GamePage
npx rsb-gen pages/Checkout
npx rsb-gen pages/Signup
npx rsb-gen pages/Signin
npx rsb-gen pages/Wishlist
npx rsb-gen pages/NotFound
<Router>
<main className="container mx-auto">
<div className="grid grid-cols-4 gap-1 mb-4">
<div className="col-span-4 my-2">Routes</div>
{Object.values(ROUTES).map((ROUTE) => (
<Link className="px-2 py-1 border rounded-sm" to={ROUTE} key={ROUTE}> {ROUTE}
</Link>))
}
</div>
<Switch>
<Route exact path={ROUTES.home}> <Home /> </Route>
{/* All routes here */}
<Route path="*"> <NotFound /> </Route>
</Switch>
</main>
</Router>

Feed data to home page.

Lets feed all the pages we created above with the necessary data consuming the exported selectors with useAppSelector .

const user = useAppSelector(selectUser)
const cart = useAppSelector(selectCartGames)
const wishlist = useAppSelector(selectWishlistGames)
const highestDiscoutsEver = useAppSelector(selectHighestDiscounts)
const wishlistIds = useAppSelector(selectWishlistGameIds)
const cartIds = useAppSelector(selectCartGameIds)
const actionGames = useAppSelector(selectActionGames)
const adventureGames = useAppSelector(selectAdventureGames)
const puzzleGames = useAppSelector(selectPuzzleGames)
const narrationGames = useAppSelector(selectNarrationGames)
  • Select the component Home.
  • You will find the props, hooks in the right side. The component Home does not have any props but it is listening to all those selectors using useAppSelector.
  • Expand the Selector → DebugValue to find the data.

Feed ‘em all!

// BrowseGames.tsx
const { data: games } = useAppSelector(selectBrowseGamesWithWish)
// Checkout.tsx
const { data: gamesInCart } = useAppSelector(selectCartGames)
// Wishlist.tsx
const { data: wishlist } = useAppSelector(selectWishlistGames)
const { data: wishlistIds } = useAppSelector(selectWishlistGameIds)
// Library.tsx
const {data: { uid }} = useAppSelector(selectUser)
// Signin.tsx
const {data: { uid },loading,error} = useAppSelector(selectUser)
  • Passes the id to useGetGamePage hook which fetches the data.
// Gamepage.tsxconst gamePage = useAppSelector(selectGamePage)
const similarGames = useAppSelector(selectGamePageSimilarGames)
const { id } = useParams<{ id: string }>()
useGetGamePage(id)

Once we get our infrastructure under our UI right, plugging UI components will become a breeze.

Thank you. See you next time.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Karthick Ragavendran

Karthick Ragavendran

Fullstack engineer @ atem.green | React, Typescript, Redux, JavaScript, UI, Storybook, CSS, UX, Cypress, CI/CD.