DEV Community

Cover image for Full Redux Overview pt.1 - Actions, Reducers, Slices, Store, Dispatch, Selectors, with React and Typescript
Pedro Uzcátegui
Pedro Uzcátegui

Posted on

Full Redux Overview pt.1 - Actions, Reducers, Slices, Store, Dispatch, Selectors, with React and Typescript

Redux is a library that is based on the Flux Philosophy

Flux is a pattern, or general architecture.

Flux propses a single undirectional data flow, one at a time, this is a circular flow composed of 3 actors: Dispatcher, Stores and Actions.

Actions: Structure that describes any change in the system: Mouse Clicks, Timeout Events, Ajax Requests, so on.

Actions are send to dispatchers.

Dispatcher: Is a function where we have an action and we dispatch the function regarding to that action. And in consequence, that action is going to "modify" the state.

State: Is the global data object of our application, is divided by stores.

Store: Is a piece of the application state. Reacts to commands from the dispatcher.

Here is the simplest Flux flow:

  1. Stores subscribe to a subset of actions.
  2. An action is sent to the dispatcher.
  3. The dispatcher notifies subscribed stores of the action.
  4. Stores update their state based on the action.
  5. The view updates according to the new state in the stores.
  6. The next action can then be processed.

Why Redux?

Redux is ideal for big projects, since it helps to share data throughout the application without having to relate into state and re-renders issues. Also, if you're not using react, then you might know that JavaScript is not a very opinionated language in the way to do things, and redux provides that structure to avoid having to guess where functions do what thing.

Why Redux

Observer Pattern and the relationship with Redux

Redux can be seen as an implementation of the observer design pattern.

An Observer, defined as a Behavioral Design Pattern, let's you define a subscription mechanism to notify multiple objects about any events that happen to the object they're observing.

The observer pattern consists of 2 objects: A Publisher, that is an object that contains all of the subscribers, can add and remove subscribers, and A Subscriber, simply the object that wants to be notified about a certain event.

When an event happens to the Publisher, this is going to inform to the subscribers about it.

All subscribers must share the same interface because we don't want to couple or redefine our publisher to various types of subscribers.

Redux works in a similar way.

Reducers

You can think of reducers as some type of microservice that is going to handle a set of specific actions of a store.

Reducers and Dispatchers in Redux

In Flux, you can have multiple stores and dispatchers.

In Redux, you don't have dispatchers and you only have 1 store. Instead of adding stores, you divide this store into smaller reducers.

In Redux, a reducer is similar to a Flux Dispatcher.

In Redux, a reducer is a function that accepts an action, and the current state, and when called, returns a new state depending on which action needs to be performed.

They receive that name because the similarity with the Array.reduce() function.

Rules of Reducers

  1. They make immutable changes, this meaning that instead of updating the state, it creates a copy of the old state, modify that copy, and the put that copy as the new original.

  2. It doesn't produce side-effects, meaning that it can't be asynchronous, it can't be random (pure functions)

  3. It should update the state only using the state and action arguments.

Having a single source of data makes debugging, testing and development easier.

Redux Terminology in Depth

Actions

In Redux, the only way we change the state, is by using actions.

In practice, actions are just plain JavaScript Objects, that contains 2 things: An action type (string), and the payload (object).

{
  type: 'INCREMENT',
  payload: {
    counterId: 'main',
    amount: -10
  }
}
Enter fullscreen mode Exit fullscreen mode

Is common that we usually perform some logic before sending this action object, so is common to see that these objects are wrapped in functions as well.

function incrementAction(counterId, amount){
  return {
    type: 'INCREMENT',
    payload: {
      counterId,
      amount
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This type of functions are called action creators.

Reducers

Ok cool, we have our payload and we have our action creator function, know if we send this action to the state, how do we effectively increase the amount? How do we get the old state?

Here is where we want to introduce Reducers:

A Reducer is a function that receives 2 things: The state of the application and an action object. We already know how the action object looks like, so we can seize the action object and the state to perform the state change logic.

function rootReducer(state, action){
  switch(){
    case 'INCREMENT':
      return { ...state, counter: state.counter + action.payload.amount };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Remember, in Redux application there will be only 1 state and that state is going to have a rootReducer function, that is going to call additional reducer functions to calculate the nested state.

Middlewares

This is a more advanced topic in Redux, but we can create some sort of interception before updating the state or passing actions to the store. We can check if an action is of certain type and do something before the action is dispatched, log out the information of the action payload, etc.

Let's make a quick pause here, to mention something: Redux is all about functional programming and pure functions. Understanding those concepts is crucial to understand Redux.

Pure Functions

Pure functions are functions that only uses its arguments to return its certain value.

These functions are called pure because it will give you the same result every single time you run them.

It doesn't rely on any type of modifier, external network events, doesn't change any data structure, etc.

function square(x){
  return  x * x;
}
Enter fullscreen mode Exit fullscreen mode

When a function use some external argument, or network, or something that could potentially affect the result of the function, this thing is called a side effect.

Fetch() calls are side effects.
Events like windows resize, mouse clicks, network calls, are side effects.
Math.random() is a side efefct.

Example of an impure function

function getUser(userID){
  return UserModel.fetch("user/"+userID).then((result) => result);
}
Enter fullscreen mode Exit fullscreen mode

Why is this function impure?

  1. It contains an variable that is not related to the scope of the function (UserModel)

  2. It contains a network call that is asynchronous and is not guaranteed to return the same value forever (fetch())

Immutability

In Redux, we don't modify the state, we copy the older state, modify that copy, and set the copy as the new original.

In JavaScript, we are still very limited in terms of Immutability, let's check this example:

const colors = {
  black: "000000",
  white: "ffffff",
  red: "ff0000"
}
Enter fullscreen mode Exit fullscreen mode

This is a constant in JavaScript. The behavior is a little weird:

If we change the actual reference, it will throw an error:

const colors = {
  black: "000000",
  white: "ffffff",
  red: "ff0000"
}

colors = "something else" // This will throw an error.
Enter fullscreen mode Exit fullscreen mode

This is expected, but what if we change only a property of the constant object?

const colors = {
  black: "000000",
  white: "ffffff",
  red: "ff0000"
}

colors.red = "something else" // OK
Enter fullscreen mode Exit fullscreen mode

So this could lead to unintended behavior, and is not something very good to have when working with Redux. So instead, Redux internally uses Immer.js, a library to make things immutable!

Also, you can frooze the object using the Object.freeze() method and passing the object inside the Object.freeze() method.

Object.freeze(colors);

colors.red = "000000";

console.log(colors.red) //  Still "ff0000";
Enter fullscreen mode Exit fullscreen mode

But hey, not so fast my dear blue hedgehog fan. Object.freeze() will not make subnested objects and its properties immutable. So as you can tell, working with immutability inside JavaScript is not a very delightful experience.

Let's make our own application using redux, react-redux, and redux toolkit.

Simple Counter Application With React and Typescript

0. Create a vite project using npm create vite

npm create vite
Enter fullscreen mode Exit fullscreen mode

Select the react -> typescript preset

1. Install the react-redux dependency

  npm install react-redux
Enter fullscreen mode Exit fullscreen mode

2. Install "@reduxjs/toolkit" dependency

  npm install @reduxjs/toolkit
Enter fullscreen mode Exit fullscreen mode

3. Create a Provider an pass the store to it

Every react application that uses redux needs a provider, this provider is commonly used at the top level of our application, in our case, is going to be App.tsx, that provider coms from react-redux.

import './App.css'
import { Provider } from "react-redux"
import Counter from './components/Counter'

function App() {
  return (
    <Provider>
      <Counter/>
    </Provider>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Don't worry right now if the provider is giving you an error, or if you don't know how the Counter component looks right now, just copy that as it is.

Redux requires that a store.ts file is created under the app directory. This directory doesn't exist by default in vite, so let's create an app/store.ts file.

Copy this code into your store.ts file, is pretty much boilerplate to configure the store.

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
    reducer: {}
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

Now, we can pass that store to the provider inside the App.tsx file.

import './App.css'
import { Provider } from "react-redux"
import { store } from './app/store'
import Counter from './components/Counter'

function App() {
  return (
    <Provider store={store}>
      <Counter/>
    </Provider>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Excelent! That's all we need to do for our App.tsx file, let's now configure our slice. In this example since we're creating a counter, let's create a counterSlice.ts

But hey, BEFORE YOU DO THAT!!!!

We need to store all of our slices inside a folder called features that will be in the same level as our previous app folder.

Here is a reference image:

How the folder structure loooks

Now, let's create that counterSlice.ts file inside the features folder.

import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import type { RootState } from "../app/store";

// We need to create a type for the slice state
interface CounterState {
    value: number
}

// Define the initial state using the previously defined state type
const initialState: CounterState = {
    value: 0
}

export const counterSlice = createSlice({
    name: "counter", // name of the slice,
    initialState, // thanks to this object is that we know the type of the state
    reducers: { // Reducers are the logic on how to process certain actions
        increment: (state) => {
            state.value += 1
        },
        decrement: (state) => {
            state.value -= 1
        },
        incrementByAmount: (state, action: PayloadAction<number>) => {
            state.value = action.payload
        }
    }
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions; // We need to export the actions from the counterSlice object, actually we are exporting the reducers.

export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer; // Difference between this and the actions export.
Enter fullscreen mode Exit fullscreen mode

Yeah I know, is very overwhelming code, so let's explain what each thing does:

Lines 1 - 2
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import type { RootState } from "../app/store";
Enter fullscreen mode Exit fullscreen mode

These are simply imports of things we are going to need, the createSlice function that allows us to create a store slice, the PayloadAction type that allow us to specify the type of the expected payload, and the RootState that we are going to use to define a get our current counter value.

Lines 5 - 11
interface CounterState {
    value: number
}

const initialState: CounterState = {
    value: 0
}
Enter fullscreen mode Exit fullscreen mode

This is the type that we're going to use to declare our initialState, is very important because is going to infer the rest of the reducers.

We use immediately that type and pass it to the initialState object.

Lines 14 - 28
export const counterSlice = createSlice({
    name: "counter", // name of the slice,
    initialState, // thanks to this object is that we know the type of the state
    reducers: { // Reducers are the logic on how to process certain actions
        increment: (state) => {
            state.value += 1
        },
        decrement: (state) => {
            state.value -= 1
        },
        incrementByAmount: (state, action: PayloadAction<number>) => {
            state.value = action.payload
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

So if we recall the definition of slice, is simply a part of the state, we can see it as a property of the state object, and this property can act as its own state.

We need to specify the name of our slice, in this case simply "counter", then we pass the initialState as the second property, and then we define a reducers property object.

Here, in each reducer, we are going to define functions that are going to be used to update the state of this slice. We have 3 reducers/actions: increment, decrement, and incrementByAmount.

As you can see, we're using the state as the first parameter to perform calculations, and in the third and last function, we're using the payload to increment the value of our counter.

The rest of the lines 30 - 34 are simply exports of the actions that we're going to pass to our hooks and the reducer itself.

4. Defined Typed Hooks

Now, since we're using typescript, we need to pass the state to our selector, but since doing this every time is not a good practice, we're going to define an app/hooks.ts file that we're going to use to re-define our hooks in a way that is reusable and good for typescript.

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Enter fullscreen mode Exit fullscreen mode

So instead of calling useDispatch and useSelector directly, we now are going to use useAppDispatch and useAppSelector.

5. Pass that slice to the root state inside store.ts

Now that we have our hooks and slice ready, let's inform the root state about this new slice, we do that by specifying the new slice inside the reducer.

Note that is not common to call the reducer object properties as "slices" inside the reducers object from the store.ts, but just export default as counterReducer instead of counterSlice, but for the sake of the tutorial, we're going to use the latter.

import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "../features/counterSlice";

export const store = configureStore({
    reducer: {
        counter: counterSlice
    }
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

6. Then use useAppSelector and useAppDispatch to get and update state accordingly.

Now we can get the state using our useAppSelector hook and we can make changes by using the useAppDispatch hook.

import { useAppDispatch, useAppSelector } from "../app/hooks";
import { decrement, increment } from "../features/counterSlice";

export default function Counter(){
    const count = useAppSelector((state) => state.counter.value); 
    // state.counter -> store.ts
    // state.counter.value -> counterSlice.ts
    const dispatch = useAppDispatch(); // We use this to dispatch actions, send actions
    return(
        <div>
            <button onClick={() => dispatch(increment())}>+</button>
            <span>
                {count}
            </span>
            <button onClick={() => dispatch(decrement())}>-</button>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

We need to import our actions from the slice to pass it to our dispatch functions, then we just need to test and that's it!

Finished App

Now try it and that's it, you have your first app with react, typescript and redux!

Differences between redux, reduxjs/toolkit and react-redux.

Reduxjs

Standard library that is used to manage global state.

  1. Ton of boilerplate code
  2. Contains an actions and reducers folders that lived separated, and a store.js file.
  3. Framework Agnostic

Redux Toolkit or RTK

More opinionated and simpler library based on ReduxJS to manage global state.

  1. Is more opinionated than simpler redux about how to do things.
  2. Contains the special app and features folder to create our hooks, slices, and store.
  3. Features like extraReducers, async thunks, and slices are simpler to implement than redux.

react-redux

Library used to pass store providers to react applications and hooks like useSelector and useDispatch.

Let me know if you have any questions and stay tuned for part 2!

Github Repo: https://github.com/pedrouzcategui/react-redux-project-1/tree/main (Make sure to 💖 the repo)

Top comments (0)