It is no secret that React is one of the most popular libraries for building User Interfaces in today's day and age. I am confident most of you who read this blog have used Redux for managing the overall state of the application in your previous projects.
Ever wondered how the redux connect() function works? Or what are the various JavaScript concepts involved in writing a connect() function?
In that case, I will walk you through the JavaScript concepts involved in writing our own connect() function, which can then be integrated into the Redux library and used in conjunction.
As per the Redux documentation, connect()() function returns
The return of connect() is a wrapper function that takes your component and returns a wrapper component with the additional props it injects. In most cases, the wrapper function will be called right away, without being saved in a temporary variable: export default connect(mapStateToProps, mapDispatchToProps)(Component).
First, let's take a look at Higher Order Functions in JavaScript.
What are Higher Order Functions?
JavaScript treats functions as First Class Citizens, which means that a function can return another function, or a function can be passed as a parameter to other functions or even store function as a value in a variable.
Basically, Higher Order Functions are just functions that return another function or which accept a function as a parameter.
Redux's connect() function is a Higher Order Function that takes two functions as parameters (mapStateToProps and mapDispatchToProps), and it also returns a function that wraps the component.
const mapStateToProps = state => {
return {};
}
const mapDispatchToProps = dispatch => {
return {};
}
export default connect(mapStateToProps, mapDispatchToProps)(OurComponent);
Now that we have seen the above implementation of Redux's connect() function, we know that connect() is an Higher Order Function. Before writing our own connect() function, we need to learn about closures and currying.
Currying
Currying is a process in functional programming in which we can transform a function with multiple arguments into a sequence of nesting functions. It returns a new function that expects the next argument inline.
Here's an example in JavaScript:
rrying-1.js
function multiply(a, b) {
return a * b;
}
// Generally, we will call the above function as multiply(1, 2)
// Lets make this function as a curried one
function multiply(a) {
return (b) => {
return a * b;
}
}
// We can call the curried multiply function as follows
// multiply(1)(2);
Confused? How does this concept apply to real-world scenarios. Let me give you a scenario.
In our application, there is a case where the result of some calculations has to be doubled. We typically did this by passing the result with 2 as arguments to the multiply function in the following way: multiply(result, 2);
A function can be returned from currying, so it can be stored and used with other sets of parameters if needed.
function multiply(a) {
return (b) => {
return a * b;
}
}
// Returns a function, which can be used with other set of parameters
const double = multiply(2);
// Using curried function with result, instead of passing same argument again and again.
const doubledResult = double(result);
Hopefully, you got the idea of how redux implements the connect()() function, using currying.
export default connect(mapStateToProps, mapDispatchToProps)(OurComponent);
Closures
Closures simply refer to the scope of the outer function being accessible by the inner function, even after the outer function has been executed and removed from the call stack.
Lets suppose we have an outer function A and inner function B.
function A() {
const msgFromOuterFn = 'I am from Outer function scope';
function B() {
console.log(msgFromOuterFn);
}
return B;
}
// A returns a function B, In JavaScript when ever any function completes its execution, its scope is removed from the heap. So all the variables declared in its scope won't be available once its execution is done.
const returnedFn = A();
// A() completed its execution, so the value msgFromOuterFn will not able available.
// With JS Closures, even the outer function completed execution, inner functions are able to access the outer functions scope.
console.log(returnedFn());
// Will print its value, instead of throwing an error
_From the concept of Higher Order Functions, Currying, we learned that the connect()() function is a HOF (Higher Order Function) that takes two functions as parameters and returns an anonymous function, which we use to wrap our component, by calling it using Currying.
Hence connect() is an outer function, whereas anonymous function returned is an inner function, so the props passed to connect() can be accessed by anonymous inner function, even after connect() has completed its execution using closures.
Now that all of these are in place, let's move on to writing our own connect() function_
Let's write our own connect() function
We are going to use a starter application counter, which has increment/decrement actions connecting to a redux store. So the plan is to write our own connect function first, and then integrate the working application with it.
The GitHub link of the counter application is as follows:
A simple counter application where the counter value is stored at redux store, which can be incremented or decremented by dispatching a redux action and updating the reducer. The Counter component is connected to redux store using react-redux connect() function.
Our understanding is that connect() is an HOF (Higher Order Function) that takes two functions as arguments and returns an anonymous function. Let's build on this idea.
// connectFn.js file
const connectFn = (mapStateToProps, mapDispatchToProps) => {
return () => {
}
}
export { connectFn };
Now, with the Anonymous Function receiving our component as an argument, we can pass it through with Currying. Next, we'll create our anonymous class component within the Anonymous Function, and the class will be returned by the Anonymous Function.
// connectFn.js file
import React, { Component } from 'react';
const connectFn = (mapStateToProps, mapDispatchToProps) => {
return (WrappedComponent) => {
return class extends Component {
render() {
return (
<WrappedComponent />
);
}
}
}
}
export { connectFn };
Here, we are using anonymous class to return our WrappedComponent inside of an anonymous function based on the HOF pattern.
We can now pass the component props along with the props generated by mapStateToProps and mapDispatchToProps. The implementation states that mapStateToProps requires an overall redux state and component props as parameters, while mapDispatchToProps requires a dispatch function and component props as parameters.
const mapStateToProps = (state, ownProps) => {
return {};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {};
}
The component props can be accessed with this.props, but how do we get the state and dispatch method of the redux store?
In the process of integrating redux into our application, a store will be created. We will export that store and import it in our connectFn file. We can access them using that store object.
// store.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export { store };
import React, { Component } from 'react';
import { store } from './redux/store';
const connectFn = (mapStateToProps, mapDispatchToProps) => {
return (WrappedComponent) => {
return class extends Component {
render() {
console.log(this.props)
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
);
}
}
}
}
export { connectFn };
There's still work to do. At this point, you may observe component is rendered on screen without any errors, however when clicking on increment/decrement the value of counter does not update. It is because we have to re-render a component whenever its state changes.
We can do this by subscribing to the store and rendering it whenever state change happens.
import React, { Component } from 'react';
import { store } from './redux/store';
const connectFn = (mapStateToProps, mapDispatchToProps) => {
return (WrappedComponent) => {
return class extends Component {
unsubscribeTheStore = null;
componentDidMount() {
this.unsubscribeTheStore = store.subscribe(this.handleStateChange);
}
componentWillUnmount() {
this.unsubscribeTheStore();
}
handleStateChange = () => {
this.forceUpdate();
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
);
}
}
}
}
export { connectFn };
We can import the connectFn and can be used as follows:
export default connectFn(mapStateToProps, mapDispatchToProps)(Counter);
That's it!!! We built our own connect() function and integrated it with the Redux store.
Final code in the Github repo
Hope it's useful
A ❤️ would be Awesome 😊
Top comments (0)