DEV Community

Cover image for Redux Without React
vedanth bora
vedanth bora

Posted on

Redux Without React

In this blog i will try to explain redux library without react. Just Redux.

Why? Because if we understand redux then we’ll understand react-redux (redux bindings for react) better.

So Redux is a relatively very small API

  • compose
  • createStore
  • bindActionCreators
  • combineReducers
  • applyMiddleware

Yes, just these five functions or methods. We will understand each one by one.

Let’s start with compose.

compose

Its just a utility that come along with redux, which is just a javaScript function that takes functions as arguments and returns a new function that executes them from right to left. Yup thats it.

To understand that better, lets say we have a have a bunch of functions that take a string

const makeUppercase = (string) => string.toUpperCase();
const logString = (string) => console.log(string) 
const boldString = (string) => string.bold();
Enter fullscreen mode Exit fullscreen mode

We could call them all like this:

logString(makeUppercase(boldString("redux")));
Enter fullscreen mode Exit fullscreen mode

But, let’s say if we wanted to pass this function as an argument to another function what would we do?

const boldenTheStringAndUppcaseItThenLogIt = (string) => 
logString(makeUppercase(boldString(string)));
Enter fullscreen mode Exit fullscreen mode

This could take too long , so we compose . Which helps use make a new function that will execute all the function passed as arguments to compose(args)

const boldenTheStringAndUppcaseItThenLogIt = redux.compose(
logString , makeUppercase, boldString
)

boldenTheStringAndUppcaseItThenLogIt("")
Enter fullscreen mode Exit fullscreen mode

this will execute the functions from right to left(boldString → makeUppercase → logString) as “redux” as argument sting, and we’re done with compose. That is what compose is.

So far we have covered 20% for the redux API


Now let’s understand createStore . Create store creates a store. A store is where we keep all our state.

But it doesn’t just create a store, however

// reducer is a function that we need to pass to the createStore,
// we will cover what a reducer shortly..

let store = redux.createStore(reducer) 
console.log(store)

// dispatch: ƒ dispatch() {}
// subscribe: ƒ subscribe() {}
// getState: ƒ getState() {}
// replaceReducer: ƒ replaceReducer() {}
Enter fullscreen mode Exit fullscreen mode

four more functions

  • dispatch
  • subscribe
  • getState
  • replaceReducer

But, what is a reducer after all ?

A reducer is also just a function that takes in two arguments

  • state (state of the application)
  • action (an action is event for example , web socket messages, action functions , etc.)

and it returns the new state.

so basically ,a reducer is a function where the first argument is the current state of application and the second is something that happened. Somewhere inside the function, we figure out what the new state of the world should to be based on whatever happened.

// this is not how you should write a reducer , just for example
const reducer = (state, action) => state //(new state)
Enter fullscreen mode Exit fullscreen mode

Let’s try to understand this with an example. Let’s say we have a counter app, and we want to increment the counter, we will have an increment action.

now, actions are also just functions, and they need to have only have type,

const initialState = { value: 0 };

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 };
  }

  return state;
};

// which can also be written as

const initialState = { value: 0 };

const INCREMENT = "INCREMENT"; // 

const incrementCounterAction = { type: INCREMENT }; // only requires type , 
 // others are optional
 // payload : {}, meta: {}, error.
const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 };
  }

  return state;
};
Enter fullscreen mode Exit fullscreen mode

Alright, there are a few things that we need to understand here.

  • You'll notice that we're creating a new object rather than mutating the existing one.
  • This is helpful because it allows us to figure out what the new state is depending on the current state
  • We also want to make sure we return the existing state if an action we don't care about comes through the reducer.

You will also notice we made a constant called INCREMENT. The main reason we do this is because we need to make sure that we don’t accidentally mis-spell the action type—either when we create the action or in the reducer.

Let’s say we have a counter which has an increment button and add input , which increment the counter and add to the counter whatever value we put in the input.

const initialState = { value: 0 }; // state of the application

const INCREMENT = "INCREMENT"; // constants 
const ADD = "ADD"; // constants

// action creators (fancy name for functions)
const increment = () => ({ type: INCREMENT });
const add = (number) => ({ type: ADD, payload: number });

const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 }; // new state
  }

  if (action.type === ADD) {
    return { value: state.value + action.payload }; // new state
  } 

  return state; // default state 
};
Enter fullscreen mode Exit fullscreen mode

So far we have understood compose and what createStore does and the four functions it creates, what a reducer and action is. But we need to still understand the four functions createStore creates.

So lets start with

  • getState (this function returns the current value of the state in the store)
  • dispatch (how do we get actions into that reducer to modify the state? Well, we dispatch them.)
const store = createStore(reducer);

console.log(store.getState()); // { value: 0 }
store.dispatch(increment());
console.log(store.getState()); // { value: 1 }
Enter fullscreen mode Exit fullscreen mode
  • susbscribe - This method takes a function that is invoked whenever the state in the store is updated.
const subscriber = () => console.log('this is a subscriber!' store.getState().value);

const unsubscribe = store.subscribe(subscriber);

store.dispatch(increment()); // "Subscriber! 1"
store.dispatch(add(4)); // "Subscriber! 5"

unsubscribe();
Enter fullscreen mode Exit fullscreen mode
  • replaceReducer used for code splitting

bindActionCreators

Earlier in the blog we read about action (which are functions). So guess what bindActionCreators does? ... binds actions functions together.!!

In the example below we have actions and reducers

const initialState = { value: 0 }; // state of the application

const INCREMENT = "INCREMENT"; // constants 
const ADD = "ADD"; // constants

// action creators (fancy name for functions)
const increment = () => ({ type: INCREMENT });
const add = (number) => ({ type: ADD, payload: number });

const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 }; // new state
  }

  if (action.type === ADD) {
    return { value: state.value + action.payload }; // new state
  } 

  return state; // default state 
};

-----------------------------------------------------------------------------

const store = createStore(reducer);

/// notice we have to do this like this everytime 
store.dispatch(increment());

Enter fullscreen mode Exit fullscreen mode

Notice we have to dispatch the actions store.dispatch(increment()) every time, this could be tedious in a large application.

we could do this in a cleaner way like this

const dispatchIncrement = () => store.dispatch(increment());
const dispatchAdd = (number) => store.dispatch(add(number));

dispatchIncrement();
dispatchAdd();
Enter fullscreen mode Exit fullscreen mode

or we could use compose . remember compose for earlier in the blog ?

const dispatchIncrement = compose(store.dispatch, increment);
const dispatchAdd = compose(store.dispatch, add);
Enter fullscreen mode Exit fullscreen mode

We could also do this using bindActionCreators , takes two arguments

  • action creators (action functions)
  • dispatch
const actions = bindActionCreators(
  {
    increment,
    add,
  },
  store.dispatch
);

actions.increment();
Enter fullscreen mode Exit fullscreen mode

Now we don’t have to use bindActionCreators all the time, but it’s there.

With that we have completed 60% of the redux API.


combineReducers

Guess what this does ? Yes, combines reducers. When we have big applications we have many reducers.

For example when we have a blog application. We would have reducers for storing the user information, reducers for storing the blogs, reducers for storing the comments.

In these big application we split the reducers into different files. combineReducers is used when we have multiple reducers and want to combine them .

Say we have an application with users and tasks. We can assign users and assign tasks

const initState = {
  users: [
    { id: 1, name: "ved" },
    { id: 2, name: "ananya" },
  ],
  tasks: [
    { title: "some task", assignedTo: 1 },
    { title: "another task", assignedTo: null },
  ],
};
Enter fullscreen mode Exit fullscreen mode

reducer file


const ADD_USER = "ADD_USER";
const ADD_TASK = "ADD_TASK";

const addTask = title => ({ type: ADD_TASK, payload: { title } });
const addUser = name => ({ type: ADD_USER, payload: { name } });

const reducer = (state = initialState, action) => {
  if (action.type === ADD_USER) {
    return {
      ...state,
      users: [...state.users, action.payload],
    };
  }

  if (action.type === ADD_TASK) {
    return {
      ...state,
      tasks: [...state.tasks, action.payload],
    };
  }
};

const store = createStore(reducer, initialState);

store.dispatch(addTask("Record the song"));
store.dispatch(addUser("moksh")); // moksh is my brother's name 😃 

console.log(store.getState());

Enter fullscreen mode Exit fullscreen mode

It would be nice if we could have two different reducers for the users and the tasks

const users = (state = initialState.users, action) => {
  if (action.type === ADD_USER) {
    return [...state, action.payload];
  }

  return state;
};

const tasks = (state = initialState.tasks, action) => {
  if (action.type === ADD_TASK) {
    return [...state, action.payload];
  }

  return state;
};

// now we need to combine the reducers and we do that using combineReducers
const reducer = redux.combineReducers({ users, tasks });

const store = createStore(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode

Fun Fact!: All actions flow through all of the reducers. So, if you want to update two pieces of state with the same action, you totally can


applyMiddleware

Theres a lot of stuff that Redux can not do by itself, so we can extend what Redux does using middleware and enhancers.

Now what are middleware and enhancers ? They’re just functions that let us add more functionality to redux.

To be more accurate, enhancers are functions that take a copy of createStore and a copy of the arguments passed to createStore   before passing them to createStore. This allows us to make libraries and plugins that will add more capabilities how the store works.

We see enhancers in use when we use the Redux Developer Tools and when we want to dispatch asynchronous actions.

The Actual API for createStore()

createStore() takes one, two, or three arguments.

  • reducer (required)
  • initialState (Optional)
  • enhancer (Optional)

Let’s try to understand this with an example, below is an enhancer that monitors the time it takes to update a state.

const monitorUpdateTimeReducerEnhancer = (createStore) => (
  reducer,
  initialState,
  enhancer
) => {
  const monitorUpdateTimeReducer = (state, action) => {
    const start = performance.now();
    const newState = reducer(state, action);
    const end = performance.now();
    const diff = round(end - start);

    console.log("Reducer process time is:", diff);

    return newState;
  };

  return createStore(monitorUpdateTimeReducer, initialState, enhancer);
};

const store = createStore(reducer, monitorReducerEnhancer);
Enter fullscreen mode Exit fullscreen mode

If its still not clear , let look at another example that logs the old state and new state using enhancers.

const reducer = (state = { count: 1 }) => state;

const logStateReducerEnhancer = (createStore) => (
  reducer,
  initialState,
  enhancer
) => {
  const logStateReducer = (state, action) => {
    console.log("old state", state, action);
    const newState = reducer(state, action);
    console.log("new state", newState, action);

    return newState;
  };

  return createStore(logStateReducer, initialState, enhancer);
};

const store = createStore(
  reducer,
  logStateReducerEnhancer)
);

store.dispatch({ type: "old state" });
store.dispatch({ type: "new updated state" });
Enter fullscreen mode Exit fullscreen mode

Ok cool, but what is applyMiddleware used for? it’s used for creating enhancers out of chain of middleware. What does that mean , maybe be some code would help understand better.

const enhancer = applyMiddleware(
  firstMiddleware,
  secondMiddleware,
  thirdMiddleware
);
Enter fullscreen mode Exit fullscreen mode

So Middleware have the following API

const someMiddleware = (store) => (next) => (action) => {
  // Do stuff before the action reaches the reducer or the next piece of middleware.
  next(action);
  // Do stuff after the action has worked through the reducer.
};
Enter fullscreen mode Exit fullscreen mode

next is either the next piece of middleware or it's store.dispatch. If you don't call next, you will swallow the action and it will never hit the reducer.

Here is an example of the logState middleware that we looked at earlier using this

const logStateMiddleware = (store) => (next) => (action) => {
  console.log("State Before", store.getState(), { action });
  next(action);
  console.log("State After", store.getState(), { action });
};

const store = createStore(reducer, applyMiddleware(logStateMiddleware));
Enter fullscreen mode Exit fullscreen mode

Lets also see how we can do this with the monitorUpdateTime

const monitorUpdateTimeMiddleware = (store) => (next) => (action) => {
  const start = performance.now();
  next(action);
  const end = performance.now();
  const diff = Math.round(end - start);

  console.log("Reducer process time is:", diff);
};

const store = createStore(reducer, applyMiddleware(monitorUpdateTimeMiddleware));
Enter fullscreen mode Exit fullscreen mode

With this we have covered all of the Redux API. I hope this blog was helpful and you learnt something new.

Discussion (4)

Collapse
phryneas profile image
Lenz Weber • Edited on

Hi, Redux maintainer here.

Please be aware, that nowadays (no matter if with or without React), you wouldn't write any of this any more in a production app.
Store creation nowadays works with configureStore, which conveniently combines createStore, combineReducers, applyMiddleware and compose.
Reducers are nowadays written with createSlice, which removes switch..case statements, immutable reducer logic and ACTION_TYPE constants from the picture.

Please give Why Redux Toolkit is How To Use Redux Today a read.

Collapse
johnmunsch profile image
John Munsch

The other reason to explain this independently is because tons of us do not use React. I use Redux Toolkit, but I use it with Angular at work and with Web Components for my own projects.

State Management is a thing independently of whatever component framework you might use.

Collapse
gosukiwi profile image
Federico Ramirez

I never thought of using Redux with Web Components, but I have an actual project which could benefit greatly from that 🤓 thanks!

Collapse
sojinsamuel profile image
Sojin Samuel

Bravo vedanth