Skip to main content

Store

Introduction


We are using Redux to manage the global state of the application.

Before doing a deep dive on store usage, let's first discuss what should save on the Store.

What should be placed in the store?


Ideally, we limit the information on the store to just the data that needs to be reactive and consumed by multiple unrelated components at the same time. What does this means?

Let's split this into pieces:

  • By reactive: we refer to information that will trigger a refresh of the components that are consuming it every time it's updated on the store, and thus, update the proper view.

  • By consumed: we refer to information that is read and used in some way by a React component. Injected through the connect function.

  • By multiple unrelated components: we refer to more than one component that needs to access this information and are not related to each other or their relation will cause an unnecessary prop drilling effect.

  • By at the same time, we refer to what the user is actually watching at a determined moment on the screen.

If the information doesn't need to trigger changes on components, if it is not injected by multiple components, or it is not displayed by multiple components at the same time, then there's no need to save that on the Store.

Usage


Redux is mainly based on the Flux Pattern. You can read their motivation on the linked page if you want to have a deeper understanding of the patters they follow.

The biggest concepts that we will be using are described on Redux's Glossary. Please, take a moment to read and understand them properly. They are the core of the Redux store.

Bellow you will find how we implement each of these concepts (and some others) on our application:

Actions


They are nothing more than a plain javascript object. They always have an Action Type associated, that works as an ID, to identify the action itself, and they could or not have a payload of information. By payload we mean any other property you want to associate to that object.

Example

{
type: 'SET_POKEMON',
pokemon: pokemon
}

In this case "SET_POKEMON" is the type, and "pokemon" is the payload.

On our application, we generate the actions objects through Action Creators, and Action Types to avoid hard-coding the string type in many places.

Action Creators


As we will need to generate these Actions from different places, we would like to ensure that the object always follows the same contract. For this, we use Action Creators, meaning factory functions that return an Action.

These Action Creators are placed under your module/store folder in a file called {my-module-name}.action.ts.

// pokemon-randomizer/store/pokemon-randomizer.actions.ts

export const setPokemon = (pokemon: Pokemon): PokemonRandomizerActionTypes => ({
type: 'SET_POKEMON',
pokemon
});

You will find the definition of the type PokemonRandomizerActionTypes on the "Store Types" section

Action Types


To avoid repeating the string that conforms a type multiple times, we can group all the types together into a file full of exported constants, named "Action Types".

You will have a file called {my-module-name}.action-types.ts under your module/store folder, which will export one constant for each action type.

Example

// pokemon-randomizer/store/pokemon-randomizer.action-types.ts

export const SET_POKEMON = 'SET_POKEMON';

This way our action should look like follows:

// pokemon-randomizer/store/pokemon-randomizer.actions.ts

import * as types from './pokemon-randomizer.action-types';

export const setPokemon = (pokemon: Pokemon): PokemonRandomizerActionTypes => ({
type: types.SET_POKEMON,
pokemon
});

You can find the definition of the type PokemonRandomizerActionTypes on the "Store Types" section

Store Types


To make work tidy with Typescript, we will need to place all the Types for each specific action that we create somewhere. The union of all module actions will be another type.

All these Types will be placed under your module/store folder in a file called {my-module-name}.store-types.ts.

Example

// pokemon-randomizer/store/pokemon-randomizer.store-types.ts

import { Pokemon } from '../types/pokemon-randomizer.types';
import * as types from './pokemon-randomizer.action-types';

interface SetTitleAction {
type: typeof types.SET_TITLE;
title: string;
}

interface SetPokemonAction {
type: typeof types.SET_POKEMON;
pokemon: Pokemon;
}

export type PokemonRandomizerActionTypes = SetTitleAction | SetPokemonAction;

Note that PokemonRandomizerActionTypes is used as the return type of each Action Creator.

Reducers


The actual reducer functions that will reduce from one state to another. Each action type will have each case under a switch statement.

This will be placed under your module/store folder in a file called {my-module-name}.reducer.ts.

First off, before the reducer function we should define the reducer slice's state type and an initial state:

// pokemon-randomizer/store/pokemon-randomizer.reducer.ts
import { Pokemon } from '../types/pokemon-randomizer.types';

export interface PokemonRandomizerReducerState {
pokemon?: Pokemon;
}

const initialState = {};

We export the interface as it will be used later when combining the reducers.

Then, we have our reducer function with the switch statements:

// pokemon-randomizer/store/pokemon-randomizer.reducer.ts

import { Pokemon } from '../types/pokemon-randomizer.types';
import { PokemonRandomizerActionTypes } from './pokemon-randomizer.store-types';

export interface PokemonRandomizerReducerState {
pokemon?: Pokemon;
}

const initialState = {};

export function PokemonRandomizerReducer(
state: PokemonRandomizerReducerState = initialState,
action: PokemonRandomizerActionTypes
): PokemonRandomizerReducerState {
switch (action.type) {
default:
return state;
}
}

Finally, for each action type you will have a specific case that returns a new plain object, spreading all the other properties that the state contains:

// pokemon-randomizer/store/pokemon-randomizer.reducer.ts

import { Pokemon } from '../types/pokemon-randomizer.types';
import * as types from './pokemon-randomizer.action-types';
import { PokemonRandomizerActionTypes } from './pokemon-randomizer.store-types';

export interface PokemonRandomizerReducerState {
pokemon?: Pokemon;
}

const initialState = {};

export function PokemonRandomizerReducer(
state: PokemonRandomizerReducerState = initialState,
action: PokemonRandomizerActionTypes
): PokemonRandomizerReducerState {
switch (action.type) {
case types.SET_POKEMON:
return {
...state,
pokemon: action.pokemon
};
default:
return state;
}
}

We need to return a brand new object because Redux requires immutability on each state change.

Selectors


Selectors provide a way to encapsulate the access to specific properties of the global state (store) and apply some memoization techniques to improve performance.

They will be placed under your module/store folder in a file called {my-module-name}.selectors.ts.

Common Selectors

A common selector is just a simple function that traverses through the state object.

// pokemon-randomizer/store/pokemon-randomizer.selectors.ts

import { AppReducerState } from '../../../App.reducers';

export const selectPokemon = (state: AppReducerState): Pokemon | undefined => state.pokemonRandomizer.pokemon;

Convention: To easily identify them, we will prepend the word select and then the name of the property we are selecting, in this case pokemon: selectPokemon.

Memoized Selectors

To create memoized selectors we will use a library called Reselect.

// pokemon-randomizer/store/pokemon-randomizer.selectors.ts

import { createSelector } from 'reselect';
import { AppReducerState } from '../../../App.reducers';
import { Pokemon } from '../types/pokemon-randomizer.types';

export const selectPokemon = (state: AppReducerState): Pokemon | undefined => state.pokemonRandomizer.pokemon;

export const selectPokemonHiddenAbilities = createSelector(selectPokemon, (pokemon?: Pokemon) => {
if (!pokemon) {
return [];
}

return pokemon.abilities.filter((ability) => ability.is_hidden);
});

Thunks


In order to make async logic with API requests or complex operations that will require an update of a slice of the store, we will use Redux Thunk. We recommend you read the official docs first to understand how it works. You will also need to be aware of Redux Middlewares.

From the docs: Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.

They will be placed under your module/store folder in a file called {my-module-name}.thunks.ts.

We will use Thunks under 2 situations:

  • Async Operations: this will be the most common case to use thunks. It will involve calling a service to make a request to an API and make an update on the store to trigger UI changes.
// pokemon-randomizer/store/pokemon-randomizer.thunks.ts

import { pokedexService } from '../services/pokedex.service';
import { setPokemon } from './pokemon-randomizer.actions';

export const findRandomPokemon = (id: number): AppThunkAction => async (dispatch: AppThunkDispatch) => {
try {
const pokemon = await pokedexService.getPokemonAPIRequest(id);

dispatch(setPokemon(pokemon));
} catch (err) {
// catch error
}
};

We recommend you read the section API Calls - Reactive Data from the React doc

  • Complex Logic: there will be some times where you will find that you need to dispatch multiple different actions from a component, to make several changes on the store. To simplify this, you can create a sync thunk that is in charge of dispatching whatever actions you need. This case is rarely needed.
// pokemon-randomizer/store/pokemon-randomizer.thunks.ts

export const evolvePokemon = (pokemon: Pokemon): AppThunkAction => (dispatch: AppThunkDispatch) => {
try {
const evolvedPokemon = evolve({ ...pokemon });

// evolving pokemon to next level...

dispatch(setPokemon(evolvedPokemon));
dispatch(setTitle(`Congratulations ! Your ${pokemon.name} has evolved into a ${evolvedPokemon.name}`));
} catch (err) {
// catch error
}
};

NOTE 1: Notice that the Thunk function is not async in this case.

NOTE 2: This is a very simple and silly example, of course there are ways to avoid using thunks in this way. But it's worth to mention it so you can bear it in mind in case you need it.

Containers


This is the place where the glue happens. This is where we inject every needed store element into a component's props. This is where we connect a component with the Redux Store. For this we will be using a HOC provided by Redux called connect.

Every single component that needs to read information from the store or interact with it, will have, right next to the {ComponentName}.component.ts file, another file called {ComponentName}.container.ts.

Below you can find a boilerplate for any container.

You just need to change the imported and exported component name:

// pokemon-randomizer/PokemonRandomizer.container.ts

import { connect } from 'react-redux';
import { AppReducerState } from '../../App.reducers';
import { PokemonRandomizer } from './PokemonRandomizer.component';

const mapStateToProps = (state: AppReducerState) => ({});

const mapDispatchToProps = {};

export default connect(mapStateToProps, mapDispatchToProps)(PokemonRandomizer);

And of course you will need to add to mapStateToProps whatever information your component needs from the store, and to mapDispatchToProps whatever actions or thunks that your component will need to call.

// pokemon-randomizer/PokemonRandomizer.container.ts

import { connect } from 'react-redux';
import { AppReducerState } from '../../App.reducers';
import { PokemonRandomizer } from './PokemonRandomizer.component';
import { selectPokemon } from './store/pokemon-randomizer.selectors';
import { findRandomPokemon } from './store/pokemon-randomizer.thunks';

const mapStateToProps = (state: AppReducerState) => ({
pokemon: selectPokemon(state)
});

const mapDispatchToProps = {
findRandomPokemon
};

export default connect(mapStateToProps, mapDispatchToProps)(PokemonRandomizer);