Create App DevTools to enhance development productivity

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 2 years ago
Updated 2 years ago

If you find yourself filling out the same form over and over again, or working through a weird workflow to enable certain features in your app just to get things developed, then custom DevTools for your app will enhance your productivity. There are so many things you can do once you are enabled to write arbitrary development-only code in your app. In this lesson we'll see how we can wire that up to make it possible.

This is based on my blog post: Make your own DevTools

Instructor: [0:00] Our app has a feature toggles module here, and if the tacos feature is turned on, then we're going to render some tacos. That feature toggles comes from this module that we can take a look at here that comes from this app config variable. That is coming from this config JS, which is going to be loaded into this index HTML via a script tag.

[0:21] The config JS could be generated by the server at runtime dynamically or at build time. If I were to enable the tacos feature here, then we'd go to a taco spinning around. By default, we have the React logo spinning around.

[0:39] If this were truly dynamically generated by the server, then it may be a little tricky to set this feature toggle to be on. I would like to have a much better experience as a developer turning those feature toggles on and off.

[0:51] What I'm going to do is create a devtools.js file right here, and I'm going to import feature toggles from feature toggles. We'll set featureToggles.tacos to true. At the very top here, I'm going to say import DevTools, save that, and we get our taco loading right here. That's awesome.

[1:17] Now, I have a really easy way to set it up regardless of whether this is actually being dynamically generated by the server or not. However, now, if I ship to production, I am enabling the feature toggle tacos, and that goes against the idea behind feature toggles in the first place. I want to conditionally do this.

[1:34] What I'm going to do is I'm going to say if process ENV node_env is development, well, then we can do my custom a feature toggle stuff here. Then I'd come back here, and I see that it is a spinning taco. If I were to build this for production, then I wouldn't be changing that feature toggle.

[1:53] From here, there's actually a whole lot of things that I could want to do in these DevTools more than just enable and disable feature toggles. If we start adding a bunch of code and even some utilities in here, that could be increasing our bundle size by a significant amount.

[2:07] What I want to do is dynamically import the DevTools so that we don't have that in our original bundle, but instead, have it loaded only if the DevTools need to be loaded. I'm going to change things a little bit here. We're going to make a devtools/load.js file here.

[2:24] Inside of that DevTools load, I'm going to do basically the same thing that we're doing here and have a conditional loading experience. We'll say function load, and then we'll say process ENV node_env is development. Then, we're going to do a dynamic import to dynamically import the DevTools.

[2:43] Let's go ahead and make a devtools.js here. That's where I'm going to take this logic that I had inside of here. We'll copy that, paste it over here. Looks like we're going to need that import for the feature toggles. Let's grab that, paste it up at the top, and then we're going to need to come up one directory since we're down one directory here.

[3:05] Now, we can save that. We'll go back to our load here.Once it's done, then what are we going to do? Well, we could console log, or what I want to do is I want to make sure that my DevTools run before I render my app. I'm going to add a callback here. Once my DevTools are done, I want to call the callback here.

[3:24] Actually, I can simplify this a little bit and just simply pass the call back as the then handler. In fact, even if my import for the DevTools fails for some reason, maybe the DevTools module fails to load or fails to run, then I still want to run that callback. We'll just ignore the failure.

[3:40] I'm going to say finally. Once that is finally loaded, then we're going to call the callback. If it's not a development environment, then we'll just call the callback synchronously right away. We'll come down here to the bottom, and we'll export default load.

[3:55] Then, come down here to our index JS, and we're going to import load DevTools from devtools/load, and then we'll copy this and provide our callback, which will do our ReactOn.render.

[4:09] We come back here, we get that spinning taco. If we're not in development, then we're not going to get the DevTools. They also won't be included in our bundle because we're using a dynamic import here.

[4:20] To take things a step further, if we wanted to, we can make a function called install, and this could be an async function. Then we can put our logic inside of here, and we can do asynchronous logic, and then we'll export install as a function and then come down here.

[4:35] Once the import is finished, then we can say then we get our module and will say module.install, return the promise that we get back from install. We'll go ahead and call that callback.

[4:47] That will allow us to do any sort of asynchronous stuff inside of this install function. We could call an API and get some additional data. We could do all kinds of things on this initial install of our DevTools, allowing us to have a ton of power when it comes to installing and using application DevTools.

[5:05] From here, we can make a UI to control the feature toggle so we can make a function DevTools. This could return hello from the DevTools just to get us going. Let's go ahead, and import React from React and import React DOM from React DOM. Then we'll say ReactDOM.render DevTools, and we'll render it to our DevTools root.

[5:32] Let's make that DevTools root, which is a document create element div. We'll say document.body.appendChild DevTools root, and then we render to that root. If we save that, come over here and scroll down, and we get hello from DevTools. Great.

[5:50] We could position that anywhere on the page, have something pop up and give us some sort of UI to control the feature toggles or what backend we're hitting, or give us information on the latest build. All sorts of things are possible once you have this React component that's being rendered to that DevTools root, which we've appended to the document body.

[6:09] Another thing to consider here is maybe I want to have the tacos feature toggle enabled all the time, but other developers don't want to have it enabled. What we're going to make is a devtools.local.js file here. That's where we're going to do this feature toggle stuff. Let me just snatch that, come back up here, we'll put that in there.

[6:29] In here, I'm going to import devtools.local, we'll save that, and we get that enabled, which is just fine. We're still importing this directly. We want to conditionally import this. We need to take into consideration the fact that we're going to git ignore this file. In fact, I already have that set up right here,

[6:51] Because it's going to be git ignored, it could possibly be that a developer hasn't created one of these files before, especially when they're initially creating the project. This import is going to fail for them. It would be nice if we just conditionally import this.

[7:06] There's no conditional import, but because we're using webpack, we can use the require context API where we're going to specify every file from this directory down. We don't want to look at subdirectories. It's just everything in this directory.

[7:21] Then we'll provide a regex for DevTools, and then we'll escape a dot and local escape a dot and JS. That is going to get us a required DevTools local. This is a special function. We'll see we got a local path where it require devtools local.keys. That gives us back an array, we'll just grab the first element of that array. That's just a path to the local file if it exist. We can say if local, then we'll require DevTools with that local path.

[7:56] Now we're conditionally loading that file. Here we have the taco because it's in our local. If I were to rename this to example, it's no longer going to be in these keys because it no longer matches this regex that we provided. We're going to get the React JS logo instead.

[8:12] Hopefully, you see that this gives you a ton of power. Let's just go ahead and take a look at the things that we changed. We no longer need this DevTools files, we can get rid of that. We have our index JS, this is all that we changed.

[8:23] We added a load DevTools that accepts a callback, and we put our rendering inside of that callback. That allows us to load the DevTools if they need to be loaded and make sure that they're loaded before we render anything in the app.

[8:36] Here in our DevTools file, we have this load, and we load it if the processing env node_env is development. We could also load it if there's a query string that says DevTools is true or if there is something in local storage that says we want to enable DevTools.

[8:50] In here, we import the DevTools dynamically so that it's not included in the bundle for the build. If we were to enable the ability to have DevTools working in production, the users wouldn't actually suffer from having those DevTools available to the developers who want to be able to use it to triage issues in production.

[9:10] We import the DevTools, and then we call the install function. Whether or not that is successful, we're going to call the callback so that we get our app rendered.

[9:18] Here in our DevTools, we are going to have this install, which basically creates a couple React components that renders that to the page so that we can control things like feature toggles or display build information. Whatever it is that you need for your particular app.

[9:33] We also added this ability to have a DevTools local, so that you can have scripts for your local development without impacting the rest of the team because we named this with a .local.js and then .local.js is ignored because of this rule in our git ignore file.