Building chrome extenstion with multiple frames using ReactJS & Redux

Vaibhav Kumar
6 min readMar 26, 2021

Powerful chrome extensions usually utilise multiple modals and panels to give an enthralling UX. In this article, let’s deep dive on how to achieve such complexities in a very simple way using ReactJS.

For the sake of this article, we’ll be designing a chrome extenstion with a Right Side Panel, Centered Modal and Small Modal (Right Bottom).

PS: popup.html is out of the scope for this article as it ‘ll be primarily focusing on html injection via content script.

Two Minutes of Basics

For newbees, this section provides high level overview of Chrome extension. A chrome extension build consists of the following

  1. Content Script : The js file capable of reading and manipulating DOM. It can be used to inject custom HTML, listen to events from the pages and read from the DOM. It is the prime script responsible for making your extension responsive.
  2. Background script : As the name suggests, runs in the background and listen to the chrome events. It doesn’t deal with the content viewed in the webpage via extension.
  3. Manifest : A json file which contains all the information regarding extension and is processed by the browser. Can be considered as a configuration profile telling browser about details, permissions assets etc of extension.

Content script can communicate to background script and vice versa via message passing.

Let’s Begin: Overview

For the scope of this article we will be creating a react app which will manage our 3 frames (2 Modals & 1 Side Panel) with a single redux store. And we are going to use iframes to inject these views in the webpages. We will be using developing our codebase on top of the boilerplate of chrome extension with ReactJs which already handles build and bundling in an efficient way. Let’s begin with the project structure.

Link to Github Repo

  • src/ directory contains content.js and all the react components.
  • content.js injects react app into the dom and then webpack bundles and compiles react js to generate final content.js which goes into the build.
  • public/ directory contains background.js, manifest.json and global styles.
  • scripts/build.js is the one reponsible of bundling the build directory using webpack.
  • build/ the final package which can uploaded as chrome extension.

The Content file

/*global chrome*/
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store/Store';
import { Provider } from 'react-redux';
import Root from './Root';
const Main = (props) => {
return(
<Provider store={store}>
<Root/>
</Provider>
);
}
const app = document.createElement('div');
document.body.appendChild(app);
ReactDOM.render(<Main />, app);

The content file injects a react component called <Main/> into the DOM and provides redux store to the applocation. The <Root/> component is a place holder component which controls the visibility of all the frames in our app.

import React from 'react';
import {connect } from 'react-redux';
import SecondaryModal from './frames/SecondaryModal';
import PrimaryModal from './frames/PrimaryModal';
import SidePanel from './frames/SidePanel';
import {VIEW_SIDE_PANEL, VIEW_PRIMARY_MODAL, VIEW_SECONDARY_MODAL} from './codes';const Root = (props) => {
const {viewId} = props.root;
return(
<div>
{viewId === VIEW_SIDE_PANEL && <SidePanel/>}
{viewId === VIEW_PRIMARY_MODAL && <PrimaryModal/>}
{viewId === VIEW_SECONDARY_MODAL && <SecondaryModal/>}
</div>
)
}
const mapStateToProps = (state) => ({
root: state.root
});
export default connect(mapStateToProps, {})(Root);

Link to Github Repo

The Frames

<Root> component has 3 child components which are actually iframes for our chrome extension which are very similar to each other.

  1. Side Panel Component
import React from 'react';
import Frame, { FrameContextConsumer }from 'react-frame-component';
import { useDispatch } from 'react-redux';
import {SWITCH_VIEW} from '../store/Actions';
import {VIEW_PRIMARY_MODAL, VIEW_SECONDARY_MODAL} from '../codes';
const SidePanel = (props) => {
const dispatch = useDispatch();
return(
<Frame id = "side-panel">
<FrameContextConsumer>
{
({document, window}) => {
return (
<div onClick={props.onClick}>
<div>
This is Side Panel.
</div>
<div className='Link' onClick={() => dispatch({type: SWITCH_VIEW,viewId: VIEW_PRIMARY_MODAL})}>
Show Primary Modal
</div>
<div className='Link' onClick={() => dispatch({type: SWITCH_VIEW, viewId: VIEW_SECONDARY_MODAL})}>
Show Secondary Modal
</div>
</div>
)
}
}
</FrameContextConsumer>
</Frame>
);
}
export default SidePanel;

2. Primary Modal Component

import React from 'react';
import Frame, { FrameContextConsumer }from 'react-frame-component';
import { useDispatch } from 'react-redux';
import {SWITCH_VIEW} from '../store/Actions';
import {VIEW_SIDE_PANEL, VIEW_SECONDARY_MODAL} from '../codes';
const PrimaryModal = (props) => {
const dispatch = useDispatch();
return(
<Frame id = "primary-modal">
<FrameContextConsumer>
{
({document, window}) => {
return (
<div>
<div>
This is Primary Modal.
</div>
<div className='Link' onClick={() => dispatch({type: SWITCH_VIEW,viewId: VIEW_SIDE_PANEL})}>
Show Side Panel
</div>
<div className='Link' onClick={() => dispatch({type: SWITCH_VIEW, viewId: VIEW_SECONDARY_MODAL})}>
Show Secondary Modal
</div>
</div>
)
}
}
</FrameContextConsumer>
</Frame>
);
}
export default PrimaryModal;

3. Secondary Modal Component

import React from 'react';
import Frame, { FrameContextConsumer }from 'react-frame-component';
import { useDispatch } from 'react-redux';
import {SWITCH_VIEW} from '../store/Actions';
import {VIEW_SIDE_PANEL, VIEW_PRIMARY_MODAL} from '../codes';
const SecondaryModal = (props) => {
const dispatch = useDispatch();
return(
<Frame id = "secondary-modal">
<FrameContextConsumer>
{
({document, window}) => {
return (
<div>
<div>
This is Secondary Modal.
</div>
<div className="Link" onClick={() => dispatch({type: SWITCH_VIEW,viewId: VIEW_SIDE_PANEL})}>
Show Side Panel
</div>
<div className="Link" onClick={() => dispatch({type: SWITCH_VIEW, viewId: VIEW_PRIMARY_MODAL})}>
Show Primary Modal
</div>
</div>
)
}
}
</FrameContextConsumer>
</Frame>
);
}
export default SecondaryModal;

Link to Github Repo

But howcome they have a totally different position while being displayed?

The answer is each of these components have a unique id = “primary-modal” and we define a style on this id making these components placed at fixed position. Let’s have a look at it.

The Styling

Below is the snippet of public/css/root.css

body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
#side-panel{
width: 300px;
height: 100%;
position: fixed;
top: 0px;
right: 0px;
z-index: 2147483647;
background-color: #FAFAFA;
}
#side-panel iframe {
width: 100%;
height: 100%;
border: none;
}
#primary-modal {
z-index: 2147483647;
max-height: 800px;
max-width: 1200px;
height:85%;
width: 80%;
border: 1px;
top:50%;
left:50%;
transform: translate(-50%, -50%);
background-color: #FAFAFA;
position: fixed;
}
#primary-modal iframe {
width: 100%;
height: 100%;
border: none;
}
#secondary-modal {
z-index: 2147483647;
max-height: 250px;
max-width: 400px;
height: 15%;
width: 40%;
border: 1px;
top: 20%;
left:80%;
transform: translate(-50%, -50%);
background-color: #FAFAFA;
position: fixed;
}
#secondary-modal iframe {
width: 100%;
height: 100%;
border: none;
}

Link to Github Repo

Every frame has a unique id which is styled with fixed position and different shapes and sizes.

Redux

The benefit of encapsulating all these components within <Root> component is we can have one global store. To showcase this we’ll create a state variable called viewId to control the visibility of these frames. <Root> component does conditional rendering based on this .

store/reducers/AppReducer.

import {SWITCH_VIEW} from '../Actions';const initialState = {
viewId: 0
}
export default function(state = initialState, action){
switch(action.type){
case SWITCH_VIEW:
return {
...state,
viewId: action.viewId
};
default:
return state;
}
}

Link to Github Repo

Store comfiguration can be found inside store/ directory. src/Root.js reads this state to conditionally render apropriate component.

PS: You may explore Router for navigation between these components. Just to display shared state navigation was don based on a state variable.

Conclusion

Hope this makes a lot easier developing a chrome extension which can provide inticrate user interfaces by injecting iframes at various positions.

--

--