Tyler: So today, I'm going to talk a bit about React and Chrome extensions. Usually when I say that, people are like, "What?" I'm going to go into a bit of why you even want to build a Chrome extension and then how you would do it with React, Redux, and all the tools that we love.
I'm a Product Manager at GoGuardian. I was originally a Front-End Engineer brought on about a year ago. The product that I was working on I fell in love with and became a Product Manager. But I'm still pretty passionate about Front-End Engineering in general.GoGuardian is an edtech startup. We affect over 2 million students a day actually. And we do it all through a Chrome extension. The Chrome extension's job is to create a safe learning environment and also a way to focus students on what they should be doing. So the first thing I want to know is, who here has heard of Google Chrome? Please? Cool. Who here has used it? Thank you. Who here has used an extension in it? And who's built one? Awesome. That's actually really cool. So, I'm glad to hear that. Just to kind of get the prerequisites out of the way, Redux users? Anyone messed around with it? Redux? Cool. Some of these might be a little out of scope but that's fine.
Chrome is a web browser by Google and extensions allow for developers to extend the functionality of it. And also for them to have an intimate relationship with the DOM. And I'll go into more details on what that actually means. But for GoGuardian, like I said, it's for education reasons. Just in December, Chromebooks took over half of the education market which is pretty phenomenal, which is up from 1% in 2012. So they're only growing and if you're looking to get an education, it's kind of the path you want to go down. At least, that's what we bet on and it's worked for us.
Next is a UI page. UI pages are things like popups. They're the little things like LastPass or Noisli or anything like that where you can actually see a UI. Another good example is the ptions pages. So, like I said, an example would be Noisli. Noisli is an ambient sound generator. They wanted users to not to have to be on their site in order to beautiful sounds of nature or coffee shops or whatever you prefer. So they made a Chrome extension that will basically play whenever the Chrome browser is on and you've selected an ambient noise.
A good example of a company that does this as well is LastPass. Do we have any LastPass users here? Cool. LastPass is a company that allows you to quickly have passwords saved and will actually inject them into the websites for you. So if you load a website that you have a LastPass saved for, they'll actually show that little icon on the far right of the text box and allow you to—almost like it's part of the website—click on your log in and just log in for you. It's pretty nifty.
I want to go ahead and just do something basic. I have this index HTML here, probably the simplest HTML file you can see. And we have our basic ID setup and a popup JS which is what we're generating in this script's directory which is a basic React component. So far, so good. And I'm using webpack. I don't want to go into the details of webpack. That's probably eight talks worth of stuff. Just want to expect that the index JS and all the fun stuff with it goes into a file called Popup JS and let's leave at that. So our app component is really simple. It just says, 'Hello world.' Cool. I'm going to go ahead and run this.
I basically have a single gulp file that's just going to run all the different webpacks for us in each directory. Done. So to load a Chrome extension, I can just go to my extensions page, make sure developer mode is turned on and just do load unpacked extension. There we have-- I want to make sure we're in the right directory, so basic, and then our build folder. Like I said, you can see there's a Content JS, Event JS. Like I said, they magically get there with webpack. I don't want to go into details because I don't understand it. And our Popup JS. So if we select that, looks like we're missing something. You can see in the top right here that we now have our example plugin and when we click it, Hello World. So that's actually React running in the Chrome extension. Now if we want to just quickly test whether it's still working or if I'm not lying to you essentially, go ahead and click it. Let's make sure we're watching it. There it is.
So I wanted to do a basic experiment when I was first working with this. So I said, "Okay. I'm in content script. I'd love to count the number of clicks that the user does on a web page, any web page." So you start the basics that you're used to. Right. And then you can say, when someone clicks on the page, I want to set my state and add a count. Now let's go ahead and initialize that up here. (Wow. Coding diagonal is something else.) And that should have reloaded so if I go back to here, the way content scripts work is you actually have to reload the extension for them to be injected properly. There we go... So we're actually listening for the click handlers on the website, updating our React that's being injected into the page which is pretty sweet. Now if I open up a new tab, what's going to happen? What's the count going to be?
Zero. Does Anyone know why?
Content scripts are contained in their own container for each page. So even if I go to a new tab, it's never going to persist across. So suddenly, your content script is only useful in the page that's in which isn't really that useful at all. So I first tested this out and I said, 'Okay, that's fine. It's not the end of the world. At least, I have my popup page.' Right? At least that persists, obviously. So I went and I did the same thing. I said, 'Okay. On my popup page, I'm going to go and I want to do the same thing.' Actually let's copy a good portion of that. But instead of on a click handler, let's just do on an interval. Simple enough. So every one second. Let's go ahead and increment our counter. Right? No problem. There you go, it's working. I'm going to ask the question again but I feel like you guys already know the answer. What's going to happen if I click out of this and then click back on to it? What's the count?
So every time you click on that, it regenerates the entire page. It does not persist the state at all. So when you call it a popup HTML, it's actually creating instances of that every single time. So suddenly you realize that, 'Wow, building React apps, or any kind of app actually, in Chrome extensions, is not as straight forward as I thought it'd be.' And you'd be right, it's not. At GoGuardian, we love React, Redux and we thought, 'Heck, there's got to be a way to solve this.'
So what you just saw was these multiple instances of content scripts and popups and the background page. The background page we didn't talk about but we did mention it's persisting the entire time in memory. And you noticed that, wow, those things don't persist at all like seriously. Even when you click somewhere else, they don't persist at all. But luckily, the Chrome team thought of this and they said, "Okay. We'll give you something called 'messaging' between all of these different components."
There's two types of messaging. One is the one-time request. It's a way for you to send something from a popup page or a content script back to your background page and say, here's a message and get a response for it. The important part of this is the response part. The thing that sets this apart is that you can respond to it like a callback or like a HTTP request, you can get an answer. Did it work? The second way to do it is long-lived connection. This is more of your WebSocket architecture even though WebSockets do support ways to get statuses or at least Socket.io creates a wrapper for that. These do not allow you to get statuses on how your message went but they're very efficient. So you can create a long-lived connection and then post messages back and forth between the different components. Are we following so far? Cool.
So when you look at it again and you have this messaging component, you start thinking, 'Wow, that looks like a lot of UI components to me.' And that looks like a store to me. At least if you're doing React-Redux everyday, that's what I saw. Or you could even say, 'Those look like React components to me. And that looks like a Redux store to me.’ So that's actually why we built something called React-Chrome-Redux which aims to basically allow you to build Chrome extensions almost exactly like you build web apps. It takes care of all the weirdness of Chrome extensions, and I'll explain how.
So you got your popup in your background HTML page. We already said that the Redux store probably should be on the background page because it's persistent. So we built a proxy store. It's a store that basically fakes that it's a Redux store. Has the exact same API, meaning it can work with every single Redux library out there. But what it actually does is transmits all actions using a single, one-time request to the Redux store and all state updates using the long-lived connections. So you have this store that looks like a Redux store on the popup or on the content script when in reality, it's just proxying all of its request to the real store in the background page. Are we following so far? Any questions? We're going to get in to the code part of it in a sec.
Audience: "This is awesome."
Cool. I'm glad to hear that. So how are we going to get started with it? The first thing is the background. So all you're going to include is wrapStore. That's it. And you're going to wrap your Redux store with wrapStore and a port name. This is the long-lived port that we're going to be communicating over. That's it. You now set up your background page to run as a Redux store. In your popup, you're going to do something just as simple. You're going to create a new proxy store and you're going to give it the same port name to communicate over. I included a provider example from React-Redux because it has the exact same API, the proxy store, you can use it like it's an actual Redux store. And all of the actions, and all of the dispatches will be proxied through all the ports and through all the request and go to the main store in the background page.
So I'm going to do another live coding session. So the first thing I'm going to do is clean this up so that we don't have our old data blocking us. So let's go ahead and remove this, and go ahead and kill this. And I believe it's called the clicker. So same idea. We want to get all the clicks on all the web pages but we want to keep an actual count of them, and we want to show that count in the popup as well. So we want this to be like a seamless UI of counting clicks. (And I tried to sell it on the web store and it just didn't take off but that's fine. The market wasn't right.) Cool. So I'm going to go ahead and go back to clicker. The reason there's these key files here, this is available on GitHub. I'll give you guys a link with all the completed code.
So let's go into our content page first or actually let's go into our background page which is called event page in this case. The only thing I've done, I gave myself some boilerplate because I'm not a big fan of live coding either. So I have myself some boilerplates so that I didn't have to write all of it. I included a simple reducer that adds a count and then I added to the background page a simple Redux store creation. That's it. Are we good with that?
So in the background, I'm going to do, like I said before, I can just do wrapStore from React-Chrome-Redux and then I can just wrap that store with that. And then let's name the port name, React SoCal (RSC). Cool. So now, our Redux store is actually available through all the messaging ports to any components that spin up after.
Let's go to our content script. Go to that Index JS and let's do the same thing. Oh, looks like it's already in there. So you can see here that we're pulling in that store like we talked about and then we're creating it down here. Let's change the port name to match the one that we have which is React SoCal. And then we're passing it to the provider which is part of React-Redux library. How many are familiar with that? React-Redux? No? Yes?
Basically, it's a way for you to pass down stores through the context in React. Just a really nice library so you're not passing things through props infinitely. In our app JSx, we're just going to update this. Let's go ahead and change this. Instead of the state, we're going to be passing it through the props. You guys remember, it's coming from the store. So here I can actually just say, 'this.props.dispatch' which is part of Redux. And then we're going to do, I think we called it what? What did we call that? ‘Add Count.’ We're going to just do ADD_COUNT. We're dispatching an action just like you would in any other kind of web app. We're just expecting magic to happen which it will. And then we're going to pull and connect which just really simply maps props from the state of the Redux store and also gives us the dispatch so we can do actions quickly. The only thing I'm changing here is I'm going to change this to this.props.count and going to create a map states to props function.
And let's go ahead and grab those state and then just return a new prop object that'll just be our count which is equal to the state's count. And so the state count is going to be the thing that's coming out of our background page Redux store. And that's it. It should be it.
Over here, we're going to gulp watch. And let's go ahead and reload that extension from here. Clicker Build. Okay. And if I go here, oh I didn't wrap the app. So I'm getting my count like normal but now if I open up a new page, it's the same number. It's the same number because it's actually being stored in the background store, in the background page, in the Redux store. I'm using normal Redux actions in order to communicate with it. Kind of cool. So now my entire app or at least, you can see an app if it was more complicated, is communicating like a normal web app. I'm not writing any extra code other than in my index file. And when I add components, it's just like my web app. I don't have to include this React-Chrome-Redux library anywhere except at my main file in my provider.
We can do this same idea, I'm going to go ahead and steal code like I did the first time with this proxy store here. Actually, I'll just take them. Yeah, I'll just take all of it. And we can go to our popup here. Same thing, scripts, components, app, and then let's go to our index page. We're just going to replace that with app and then let's include those two libraries that we don't have in here. And the store from React-Chrome-Redux. That should be everything. We can hope.
And then in our app, we need to do the same thing. I think we can actually just steal the component straight out of content except for that middle function which we'll take care of. And then we don't want to actually listen for clicks in that. We could. You know, we'll do it. Why not? It should work fine. And so, I'm going to go ahead and—actually, I don't need to reload it. So if we look here, there it is. The same count. Click that, notice they're both going up. And if I go back out here, do that. Right. And like I said, the content scripts are injected anywhere. So even if I were to go to some website like GitHub, assume that the same stuff would be up above there because it would. I promise.
The things that I didn't go over in this talk for the sake of time, async action handling. I didn't want to go too deep into this but basically because the actions are created on the content script and on the popup side, the async actions you need to actually handle in the background so we allow a way to build aliases. So when you see a certain action come in, you can actually transform it into an asynchronous action and then process it, and that's with the custom dispatch resolver. So just like when you're using React thunk or some kind of library to deal with asynchronous actions, we can actually return the response of that back through that message to allow you to seamlessly say 'this.props.dispatch' then this, catch this. So you can actually show error messages without any weirdness of knowing where the errors are. You don't need the storm in your store or anything like that. Like I said, this is all publicly available under React-Chrome-Redux and you can also the examples here, the runnable and the clean versions in that directory.