Creating a Todo Chrome Extension with React, Custom Hooks and Local Storage

Lee McGowan
Dec 20, 2019 · 5 min read

Ever wanted to spam yourself with annoying and invasive notifications? Or remind yourself daily that you’re more likely to jerk off and eat Doritos than actually do any work?

Well, look no further. I’ve got you covered.

Chrome extensions can do all of that, and the best part is that you can’t even escape them by reloading the page. Plus, no one knows how to turn the damn things off.

In this article, we’re going to create a todo application and deploy it locally as a chrome extension. I considered doing a ‘sexy singles in your area’ one instead but realised I don’t know any sexy singles. Call me, please.

We’re going to use React for this. It’s absolutely not necessary. In fact, it’s chronically unnecessary. But what can I say? I absolutely hate myself.

Let’s get started.


  • Some basic knowledge of React
  • A little familiarity with custom hooks
  • A bunch of things you know you’ll never finish, but you still want to record them because perpetually lying to yourself is easier than facing the truth

Clone the Repo

Clone the react-starter repo.

Use npm run start to fire it up.

Visit localhost:3000.

Creating a Todo App

There are three parts to this: the main App component, a component for creating new todos & a custom useTodos hook. I’ll go through each.

<CreateTodoForm />

Image for post
Image for post

A simple form. It has a text box for creating the todo label & a button for saving it. It receives a createNewTodo prop which is a function we’ll see shortly.


Image for post
Image for post

This is the most complicated part of the app, so I’ll deal with it bit by bit.

const getTodos = () => {
const todos = window.localStorage.getItem('todos');
if (!todos) {
return [];
return JSON.parse(todos);

A function which retrieves the todos from local storage. If they come back falsy (i.e. they don’t yet exist), we just return an empty array.

The last line converts the todos to a javascript object. This is necessary because local storage holds objects as strings.

const [todos, setTodos] = useState(getTodos());

We use state here so that a re-render is triggered whenever todos changes. The initial value is set by calling getTodos.

const updateTodos = (newTodos) => {
const stringifiedTodos = JSON.stringify(newTodos);
window.localStorage.setItem('todos', stringifiedTodos);

A function which updates the new todos in local storage and then sets them in state. We need to call JSON.stringify on newTodos because local storage can only hold strings.

const setTodoCompleted = (id, completed) => {
const todoIdx = todos.findIndex(todo => === id);
const newTodos = [...todos];
newTodos[todoIdx].completed = completed; updateTodos(newTodos);

A function which sets the property completed on a given todo. First, we get the index of the todo from the todos array in state. Then, we use array spread syntax to copy the array into a new field. This is so that we don’t mutate the original value. Then, we update the completed field and finally call our updateTodos function with the updated array.

const addTodo = (label) => {
const newTodo = { label, completed: false, id: uuid() };
updateTodos([...todos, newTodo]);

A function which adds a new todo to the list. We first create the new todo object, using the label passed up from <CreateTodoForm />. We also set completed = false by default & use the npm uuid library to generate a unique id for our todo. We use this in updates, as shown in setTodoCompleted. Finally, we call updateTodos and use array spread syntax again, only this time it’s used to append newTodo to our current todos array.

The last part returns the todos array and a reference to both the addTodo and setTodoCompleted functions, which <App /> will need.

<App />

Image for post
Image for post

<App /> ties everything together. We map over the items in todos and render a label & a checkbox for each. These have onChange handlers which call our setTodoCompleted function from the useTodos hook.

We also render <CreateTodoForm /> and pass addTodo down to it.

App.css contains some basic styling. If you want to use them, they look like this:

.App {
padding: 32px;
.todo {
display: flex;
padding: 8px;
margin: 8px;
background-color: whitesmoke;
.todo p {
margin: 0;
margin-right: 16px;
.create-todo-form {
text-align: center;
.create-todo-form button {
margin: 5px;

Run the application. Everything should be working.

Deploying as a Chrome Extension

Open public/manifest.json and paste the following:

"short_name": "TodoApp",
"name": "TodoApp",
"manifest_version": 2,
"browser_action": {
"default_popup": "index.html",
"default_title": "TodoApp"
"permissions": [
"version": "1.0"

This file describes the app. The browser_action.default_popop property points to its main page, which in most cases will be index.html.

Save that, and run npm run build from the root of the app.

Now, open Chrome and navigate to chrome://extensions in your address bar. Toggle Developer Mode on at the top right of the window, and then click Load Unpacked at the top left. Find your build/ folder and hit select. Your extension should appear in the list, accompanied by a little icon in the browser extension bar, probably a white capital ‘T’. You click this to open the extension.

But it won’t work yet.

Chrome’s security policy prevents extensions from executing inline scripts, which is exactly what our app uses. In order to get around this, you have to add another line to manifest.json:

"content_security_policy": "script-src 'self' '<your-sha>'; object-src 'self'"

You may be wondering why I didn’t ask you to add this in the first place. Well, it wasn’t just to be annoying — although being annoying is a noble cause —it’s because you need to copy something from the error log chrome generated when it failed to open your extension.

Go back to chrome://extensions and click the Errors button on your extension. In the text beginning Refused to execute inline script... find the string starting with sha-256 and copy it. Everything between the two apostrophes. Now, replace <your-sha> in the text above and paste the whole thing into manifest.json.

Rebuild your extension with npm run build and load it back into chrome using the Load unpacked button. That should be it.

Great — now you can create browser extensions and the world is your oyster. You’re capable of anything. Go sail the Atlantic. Become president. Get Brexit Done.

Just don’t make another fucking colour picker.

If you want more like this, follow me here or on Twitter at @lm_writing

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Lee McGowan

Written by

I write about programming, and I write about writing. I also might write about philosophy, if I ever get ‘round to engaging my tiny brain.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Lee McGowan

Written by

I write about programming, and I write about writing. I also might write about philosophy, if I ever get ‘round to engaging my tiny brain.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

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