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();
We could call them all like this:
logString(makeUppercase(boldString("redux")));
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)));
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("")
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() {}
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)
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;
};
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
};
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 }
-
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();
-
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());
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();
or we could use compose
. remember compose for earlier in the blog ?
const dispatchIncrement = compose(store.dispatch, increment);
const dispatchAdd = compose(store.dispatch, add);
We could also do this using bindActionCreators
, takes two arguments
- action creators (action functions)
- dispatch
const actions = bindActionCreators(
{
increment,
add,
},
store.dispatch
);
actions.increment();
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 },
],
};
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());
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);
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);
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" });
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
);
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.
};
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));
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));
With this we have covered all of the Redux API. I hope this blog was helpful and you learnt something new.
Top comments (4)
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 combinescreateStore
,combineReducers
,applyMiddleware
andcompose
.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.
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.
I never thought of using Redux with Web Components, but I have an actual project which could benefit greatly from that 🤓 thanks!
Bravo vedanth