A comprehensive guide to Redux for beginners

A comprehensive guide to Redux for beginners

What is Redux ?

Redux is predictable state container for javascript apps. It can be used with any view library but is more commonly used with React. Is has a small footprint of 2KB but it helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.

Understanding the principles behind Redux

To understand a technology we first need to understand the need that invented it. After all necessity is the mother of all inventions.

If you are familiar with any front end technology such as React you would know that these technologies build UI as components. A page is a component, which has a TopNavigationBar as a child component and say 5 different buttons that do different things. Typically different teams would build these components independently and some developer puts them together to form a page that is presented to the user.

An example page with components

This sort of component design is called "separation of concerns" and is a very good design pattern. It means each component does it own thing independent of others. That behaviour can be built, maintained and tested independently by different developers. This simplifies everything.

However, in real software it is also true that different components sort of depend on each other. For example, when you clik on "Add To Cart" on a button, the cart box on the top right corner must show some number to indicate that the add to cart operation was successful.

Add to cart wireframe

Here the Add To Cart is one component and the Cart indicator on the page is seperate and yet, action on one impacted the another.

Communication between components

Now, React, Angular, Vue etc. provide standard mechanisms to to make this sort of intra component message sharing. One of them is basically creating events and letting other components subscribe to those events.

However as you build more and more complex web applications this sort of message passing and event handling becomes super complex. Not only that it is harder for any anaytics to understand why a particular component got into the state it is in.

In react for example, a parent component can easily pass data to child component using something called "props". However a child component passing data to parent needs to happen through passing callback functions into props.

Example.


const ParentComponent = ()=>{
const [parentState, setParentState] = useState(0);
const callback = ()=>{ setParentState(parentState+1); };

return (<div>{parentState} <ChildComponent onClick={callback} text="Click Me"/> </div>)

}

const ChildComponent = ({text, callback}) => {
return (
<Button onClick={()=>callback();}> {text} </Button>
);

}

In this example the parent component passes a value "text" to child and the child will end up updating the counter in parentState when user clicks the child component.

All this is far too complex to understand once your application has dozens of components that depend on each other. Especially when something goes wrong, it is lot harder to ask "why exactly did this component got into this state?"

The global state

So the problem mentioned in previous section can very easily be solved if we could somehow maintain some kind of global state, a sort of database that all components can write to and read from. In that case it is lot simpler for each component to maintain its interal state and modify the state.

In above example imagine you have 4 components.

  • Component 1 shows a counter value.
  • Component 2 when clicked increments the counter value
  • Component 3 when clicked decremients the counter value
  • Component 4 when clicked resets the counter value to 0.

Normally we will have to wire each of these components to one another by custom events handlers. But if our application has access to a local store of data then it becomes very simple.

The Component 1, just listens to the store state and displays the counter value. It does not bother itself with who is modifying the value and how.

The Component 2, 3, 4, just modify the state and never worry about who is consuming this state.

This truly makes these components indepedent and loosely coupled.

Redux as a global state store

Redux is precisely the state store that allows an app to maintain a global state that all components can write into and read from.

The following code is taken from official redux documentation that shows creation of a store to store a counter value.


import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
incremented: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decremented: state => {
state.value -= 1
}
}
})

export const { incremented, decremented } = counterSlice.actions

const store = configureStore({
reducer: counterSlice.reducer
})

// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))

// Still pass action objects to "dispatch", but they're created for us
store.dispatch(incremented())
// {value: 1}
store.dispatch(incremented())
// {value: 2}
store.dispatch(decremented())
// {value: 1}

Thinking in Redux

Involving a global state store does make things complex. Because, now you have to think of your entire app as one global state. Then every component writes logic to mutate that state. In fact all interactions lead to some mutation operation on the global store.

What if you have a component that both increments and displays a counter ? Very likely you will have to still use global store to mutate the global state and then display it as well. Without Redux you would do this by creating a local state variable but you would lose the ability to share this state outside the component.

Note that every component you build now depends on Redux store and Redux becomes the common glue that connects all your components. This means all your components are tightly coupled with Redux but losely coupled with each other.

Understanding Redux Primitives

To understand Redux, we need to understand some basic terms around Redux.

The Model

The state of your entire app may be understood as a simple Javascript object. This object is called the "Model".

This model has no setters defined on it. The only way to change the model is through "dispatching an action"


{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

This example model shows two primary variables being tracked. One is a list of TODOs. Second one is a string visibilityFilter that describes how this list may be displayed in some UI.

The Action

Action is a very simple javascript object that describe how the state is to change. When you read the list of actions, or look a log of the actions you can exactly recreate the scenario that user created through their actions.

The process of running an action is called "Dispatching An Action".


{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

So some component which wants to add a new TODO will call dispatch method like below:


dispatch({ type: 'ADD_TODO', text: 'Go to swimming pool' });

When this method is executed, the component can be certain that it will modify the global state.

The Reducer

You have to right the logic that takes and action and modifies the Model. This method is called "The Reducer". The Reducer takes two arguments.

  • Current state of the model.
  • The Action.

Now, a typical model for the entrie app could be huge. So we do not write a single reducer for the whole state but rather the parts of it.

For example the reducer that just adds a new TODO looks like following. Notice that there is no logic to update the visibilityFilter. The reducer for that would be separate.


function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}

A word of caution when using Redux

While redux is indeed one of the most popular libraries out there, it is worth noting that it does come up with its downsides.

If you are pre-rendering pages, then entire redux initial state must be passed to the client as well. This is the case with Nextjs's static optimizations. This problem can be solved but it is not straightforward.

Often you do not want to load your entire javascript in the client, you want to load it based on the need. But Redux requires all the reducers to be available at the client side, hence you will have load all of them on the client.

Redux is especially tricky to use with Nextjs.

Redux setup is extremely complicated and tricky to get right. Someone has to think about the right "model" to represent the entire state. A bug in reducer or dispatching can lead to side effects that are hard to catch.

Other alternatives to Redux

It is worth noting that while Redux is extremely powerful and useful library, you need not use it in all cases.

  • Redux is better not used if your application is simple and has very little inter-component communication.
  • React offers inbuilt mechanisms like Context now that offer viable alternative to Redux. Very likely React will end up providing Redux like features as first class citizen.
  • There are other libraries like Redux that more or less provide same functionality but they also suffer from more or less the same problems.