Official redux logo from https://github.com/reactjs/redux/tree/master/logo
Pre requisites: the reader is already knows JavaScript & React. If not, please read this first. It is a refresher into frontend engineering with React.
According to the official documentation, Redux is a predictable state container for JavaScript apps.
So to start with, let us begin with a why. It is mainly three-fold:
Definition: A function that operates solely on the passed in arguments and has no side effects is a Pure Function.
In math, we have sin(x)
. In JavaScript, we have,
const sum = (a, b) => a + b;
Because the sum
function works solely off the passed in inputs, given arguments are the same, it will produce the same output irrespective of the number of times or under what conditions we invoke it.
As in, sum(4, 5)
will always return to 9. Time and again. Without fail.
Like sin(x)
our sum(x, y)
is predictable.
Redux is based on the idea that there should be only a single source of truth for your application state, be it UI state like which tab is active or Data state like the user profile details:
{first_name: 'John',last_name: 'Doe',age: 28',}
All of these data is retained by redux in a closure that redux calls a **store**
. It also provides us a recipe of creating the said store, namely createStore(x)
.
The createStore
function accepts another function,x
as an argument. The passed in function is responsible for returning the state of the application at that point in time, which is then persisted in the store.
This passed in function is known as the **reducer**
.
This is a valid (but not exactly useful) reducer function:
export default function reducer(state={}, action) {return state;}
Redux has this concept of actions
. An action is a plain old JavaScript object of the following shape:
{type: 'SOME_NAME',payload: 'SOME FREE FORM DATA'}
For example, say we have a micro blogging site and a user posts some content. This is what the resulting action could look like:
{type: 'Add_USER_POST',payload: {content: 'A quick brown fox jumped over the lazy dog',}}
Now, this action is passed into the reducer
to come up with the updated state as a function of the passed in action
.
To handle it, let us update our reducer
to this:
export default function reducer(state = {posts: []}, action) {switch (action.type) {case 'Add_USER_POST':return {...state,posts: [...state.posts,{content: action.payload.content,}]};
default:
return state;
}
}
First, we added a default property posts
to our default state and initialised it with []
.
Next, we simply added a switch-case
block that switches on action.type
. Because our action had a type of Add_USER_POST
it will be intercepted by the 1st case in our reducer’s switch-case
and it will return a fresh object composed out of our existing state and add the newly added post to the posts array.
This returned object will then be added to the store.
One important thing to note in here is that the store can only be updated by dispatching an action.
Your App dispatches an action
, it is passed into reducer
; the reducer
returns a fresh instance of the state; the store notifies your App and it can begin it’s re render as required.
The store
exposes 3 functions: dispatch
, getState
& subscribe
.
dispatch
as the name suggests, dispatches actions to be consumed by the reducer.
getState
returns the snapshot of your application state at this moment.
subscribe
accepts a callback function that is fired for every modification / update to the state tree.
These are used by UI library specific bindings to handle the redux to app bridge. For React
, the recommended binding is react-redux
.
Say we have a Posts
component that renders a list of posts. This is how we write it:
import React, { Component } from 'react';import { connect } from 'react-redux';
class Posts extends Component {render() {const posts = this.props.posts.map((post, i) => (<li key={i}>{post.content}</li>));return (<h3>Posts</h3><ul>{posts}</ul>)}}
const mapState = state => ({posts: state.posts,});
const mapDispatch = dispatch => ({});
export default connect(mapState,mapDispatch)(Posts);
As you can see, the component Posts
simply receives the list of posts as props and renders it as received.
We define two functions: mapState
& mapProps
and pass them as callbacks to connect
.
The connect
does a getState
and provides the current snapshot of state to mapState
as argument;
it then passes dispatch
function as an argument to mapDispatch
and finally passes in the Posts
component to the function returned by the connect(x, y)
call.
Internally, it subscribes to the state changes and passes data all the way to the component to see if a re render is required.
One can argue it is too much work for a simple app and single source of truth can be implemented in pure React with state in some top level component and passing it down as props.
However, as your app grows, you add tens of more components and every single time you need to pass in data from top root all the way to leaf components. Needless to say, it can quickly become cumbersome.
Redux allows us to subscribe to any part of the state tree from any depth.
As we have seen, our Posts
component can simply subscribe to the posts
property in the store.
The Posts
component has absolutely no relation to its parent; as in it can be in an n-level depth and it’s logic or it’s parent/rendering component’s logic will not change.
Say you have a search feature that renders a SearchBar
which ultimately renders a set of SearchItems
component. Both <SearchBar/>
& <SearchItems/>
can directly subscribe to the store.
Tomorrow, there can be a requirement to move the search inside the Posts
component and other than the CSS changes, component hierarchy wise, all we have to do is cut-paste.
Because reducers are all pure functions, there is no mocking effort required when we write unit tests for them.
Simply pass in the action
and expect a state snapshot in return. Your application logic can now be fully unit tested and as such bulletproof.
Take redux for a spin at stackblitz. Alternatively, play around with the embed here (scroll down till end).
Thanks for reading.
Hit *clap* if you think this was worth your while :)
Find me on linkedin or revert back to me on comments.