Building chrome extenstion with multiple frames using ReactJS & Redux

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

  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

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

  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

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

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

Keeping It Simple & Stupid

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store