Redux has revolutionized the world of JavaScript state management with its unidirectional data flow and the predictability it offers. But as Master Yoda of Star Wars says, “Always two, there are. No more, no less,” let us journey into the advanced aspects of Redux which take the form of the second entity of Yoda’s wisdom. Today, we’ll use our accumulated knowledge to inspect the hidden compartments and less-traveled paths of this incredible tool.

Let’s start with Redux middleware, one of the powerful entities that live in Redux’s architectural cosmos. Middleware serves as a middle ground between an action being dispatched and the action reaching the reducers. One famous middleware is redux-thunk, published under the Redux.js official Github repository. Redux-thunk allows for delaying the dispatch of an action, or to dispatch only if certain conditions are met.

function fetchData() {
  return function(dispatch) {
    dispatch(fetchDataBegin());
    return fetch("/data")
      .then(handleErrors)
      .then(res => res.json())
      .then(json => {
        dispatch(fetchDataSuccess(json.data));
        return json.data;
      })
      .catch(error => dispatch(fetchDataFailure(error)));
  };
}

Here, the nested function returned by fetchData() will be fed into dispatch(). Thanks to redux-thunk, this doesn’t throw an error. Instead, Redux-Thunk provides a third-party extension point between dispatching an action and the moment it reaches the reducer.

Redux’s main selling points include predictability, however, with great power comes great responsibility. To quote Dan Abramov, the creator of Redux, “Handling more state than you need can make your app code more difficult to understand.”

One common pitfall while using Redux is overusing it. As Redux is explicit and verbose, using it for every small piece of state can add unnecessary complexity and make the code hard to follow. Certain state might be better off at component level and does not necessarily need Redux.

We can also use selector functions to efficiently compute derived data to avoid repeating code and decouple state shape from the component. Using this technique, we can avoid unnecessary re-rendering (and thus a potential performance hit) by preventing unnecessary state updates.

import { createSelector } from 'reselect'

const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos

export const getVisibleTodos = createSelector(
  [getVisibilityFilter, getTodos],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  }
)

This code snippet is an example from Reselect GitHub library, a selector library for Redux.

Following Mark Erikson’s advice, one of the maintainers of Redux, use action creators to encapsulate the details of “how” a certain action is dispatched, and keep this logic inside of the action creators, not the components.

Redux brings significant value to larger applications where managing state can become incredibly complex. It establishes strict rules on how to update application state, making state changes predictable, easy to understand and debug.

Looking at the modern advancements, Redux Toolkit helps simplify a lot of Redux’s complexities. As per Redux Toolkit’s official documentation, “Its purpose is to help developers write Redux logic with better practices and simpler code.” It provides powerful methods like configureStore, createSlice etc. that encapsulates Redux best practices within smaller, simpler APIs.

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1
  }
})

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

store.dispatch(counterSlice.actions.increment())

In Redux, immutability is key to track changes in the state. It’s essential to avoid direct mutation of state data, a principle which can be challenging to uphold. Libraries like Immer used together with Redux can make the task of modifying state while keeping it immutable simpler.

import produce from "immer";

const baseState = [
  {
    todo: "Learn typescript",
    done: true
  },
  {
    todo: "Try immer",
    done: false
  }
];

const nextState = produce(baseState, draftState => {
  draftState.push({todo: "Tweet about it"});
  draftState[1].done = true;
});

In conclusion, Redux is a workhorse that elevates state management and gives predictability in your applications. Like any tool, mastering Redux takes time and practice. Armed with this knowledge, you can explore Redux’s deepest parts with a renewed sense of adventure. The joy of coding comes from solving problems, and Redux offers an abundance of opportunities to develop elegant solutions. Happy Coding!