Custom Hooks in React: The Ultimate UI Abstraction Layer - Tanner Linsley | JSConf Hawaii 2020

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

ooh nice! queueing up to watch :)

👍︎︎ 1 👤︎︎ u/swyx 📅︎︎ Mar 31 2020 đź—«︎ replies
Captions
So, like I said, my name is Tanner Linsley. I am from Utah and I love JavaScript so much. I also love React even more. I'm happy to be able to talk about React at a JavaScript conference. I'm on GitHub probably way too much, writing way too many open-source libraries. I'm also on Twitter and YouTube, so you can come say hi to me there if you want. I know that you're all probably feeling like... you can't see my screen. Yeah. You're probably feeling a little bit like my son here, just really tired and wanting to consume that last little bit of JavaScript. Just do your best. I know it's hard but we're going to make it through it together. Because today I'm talking about React hooks and more specifically custom React hooks and I'm so excited about this and I hope you are too. I know it's been a noisy year. You guys probably feel like... whether you love React or not you probably feel this way. So, I'm going to do my best today to make sure that this is not just another React hooks tutorial. It's going to be fast because I have a lot of content to go over and there's going to be a lot of code. Hopefully it's a lot of fun. I'm going to be saying “use” a lot. So, it's hard, it's a tongue twister. You'll see. So, before we get into the bulk of my talk, let's go through React Hooks Basics really quickly. Classes are out. We no longer write with classes. Everything is just a function now and that is so nice. It makes us so that components that used to look like this to use State don't have to look like that anymore, we can just use useState. It's so much better in my opinion or we can use useReducer. Class Lifecycles are no longer a thing. I'm so glad to be done with these things. Also, we don't have to do manual change detection anymore with the old class lifecycles. We can just use the useEffect function from React, pass it some dependencies and everything will just synchronize for us. It also comes built-in with memoization and computed state and things around memoizing values and functions. Where we could do this with React before but now it's built in. So, we can make sure that we're only computing or doing work when we need to be doing it. And last, the most important thing that I'd like to talk about today is Custom Hooks, which a custom hook is really just a function that runs inside of a component that can run other hooks or other functions like useState, useReducer, whatever. And they can be recursive, they can go forever. You can have a custom hook call another custom hook call another custom hook call another custom hook and everything will just come back in and it just works. It's amazing. It makes patterns like this that before we had to nest all these callbacks and create higher-order components and render props and functions as children, all that gets to go away and we're simply left with a couple of function calls that we can make inside of our components. Custom hooks are so powerful. I believe that they give us a couple more affordances that we didn't have when we were using class components. I think that they encourage us to build our own solutions because it makes building our own solutions easier. The API is more expressive. I also believe that building hooks, custom hooks is way more portable and shareable than building custom components and trying to move logic around with components. The other great thing is that custom hooks can be component aware and abstract essentially anything that we want them to. This makes them very powerful for integrating with essentially anything we want to integrate in the outside world away from React. And last but not least, I think that they highly encourage rapid iteration for some of the reasons that I'm going to talk about today. So, the biggest reason that I believe that all those things are true is that hooks, that with the introduction of custom hooks, I think that we're going to see or have already seen a return to components handling user interface and hooks handling business logic. And if this seems like a no-brainer, you're ahead of the curve and if it doesn't, that's what I'd like to talk about today. Through so many migrations of old projects to hooks going 100% hooks and new greenfield projects where I was able to experiment with new patterns around custom hooks and see how far can we push the limits of this new API, I've discovered a couple of use cases that have really sparked new interest for me with this hooks API. Most of them around building portable UI utilities, a few of them, well actually a lot of them, around managing global state and server state and also encapsulating business logic for our applications inside of these custom hooks. And I want to share with you a couple of these use cases today to show you how custom hooks can really change the way that you think about writing your applications and to be more productive. Why not start off with something that is so hot right now, you can't ship an app without it and it sometimes is the first thing that we all think about, probably the most important thing we all think about, that's Dark Mode. You can't ship without it, right? This is so hot right now. Everybody wants a dark mode. Everybody's got that toggle at the top of their site, you go between light and dark mode. It's pretty easy to do. You can just set up some state and toggle between it with a function set is dark to true or false. It's really that easy. I wanted something a little more sophisticated for my dark mode. I don't know if you guys have ever seen matchMedia, but it's an API that you can use to match CSS media queries in JavaScript and detect certain things about the users environment. Right here we have a matchMedia that's detecting whether the user prefers a light or a dark theme and we can actually set up our state to start with that, so that if the user has a dark or light theme preference on their device, our state now starts with that preference and they can toggle between it. But who has this enabled on their computer? This auto mode. I do. And I love it and I thought wouldn't it be amazing if we could make it so our light and dark themes would actually react to that automatic mode on our devices. Turns out it's not too hard. We can take an effect and put our matchMedia inside it and listen to changes to that matchMedia and update our dark variable whenever that changes. And if we put all that together inside of our app right here, it's all just handled for us now. The Sun sets, we go into dark mode. The Sun rises, we go into light mode. It's actually pretty fun to play with. But the problem I see here is that a lot of this logic is now taking up space in my app component and I don't really like that talking about “clean code” opinions here, but this I see is a great example of where we can make our first custom hook. We can actually just take all that logic and rip it out into its own file, we'll call it useDarkMode and everything in here is exactly the same except for what we're returning is dark at the very end and that makes this useDarkMode hook extremely versatile. In our app now we can import that file call useDarkMode and it's going to give us true or false, but it's not just true or false, it's true or false that changes over time and all of the lifecycle and updating and all of the intelligence of that dark mode is built into that hook and all we have to do is call it and that is our first custom hook for today. That's when everybody claps. So, the next thing that I thought every application needs, everybody has written this a thousand times, clicking outside of an element and I don't know why the browsers don't make it easier to do this natively, but I feel like I'm implementing this every day. Clicking outside of a menu or a modal or whatever, right, and then you want to do something to it. I thought, there's not a better excuse to write a custom component for this and I thought we might as well just make the assumption that this is going to be a custom hook right out of the gate. So, I assumed that I could take a ref created by useRef, put it on to whatever element I want to keep track of and then have a handle or something like console logging, hey, you clicked outside of this thing and I could pass those two things to this useClickOutside hook and it would just work and I wrote this actually before I wrote the useClickOutside hook to see how reliable it was to design APIs around hooks before even writing them. It turns out it wasn't too difficult. We start with the function signature, we throw in an effect that's going to set up our event listeners on the document for when we click on the document. Those call our internal listener here that checks to see if the target we clicked is inside of the element that we're tracking, and if it isn't, it calls our callback. Turns out it just works and we would probably use this for a little bit and think Wow! This is great, I'm a genius, but eventually you might notice something very strange. There's something fishy going on here and it has to do with the useEffect dependencies. Now if you're using the eslint-plugin-react-hooks, which you should be, 99% of the time it does exactly what you want it to, it auto fills Effect dependencies for your hooks. So helpful, I love it. You can just hit Save and if you have eslint setup it will just do the changes for you. And that's what it did here when we built our hook. Here on the right, you can see it automatically filled in the callback in the elRef down there in our dependencies but the problem is that the callback is changing every single render from where we're using it on the left side here onClickOutside. We're creating it every single time which means that those event listeners are firing off. Every single time that we render we're adding and removing event listeners. That is not cool. Well, it turns out it's pretty easy to just wrap that in a useCallback from React and it's going to make it so that that callback function doesn't change unless the dependencies change. There are no dependencies yet. So, nothing's going to change and it will stop creating those event listeners over and over and over, but this begs the question for me, why do I have to have that useCallback around that function? This probably in advance educates, but I feel like there's nothing inside of my useClickoutside hook that cares about this callback changing and I decided that that's part of the API. So, I wanted to get rid of it and one of the easiest ways to do that is to instead of referencing the callback directly, we can use this pattern of tracking the latest version of that callback in a ref itself, and when we do that the callback ref is now added to the dependency array which will never change but the value that we hold inside it will and that way we're only setting up and tearing down that event listener when we actually need to when the element that we're tracking actually changes. So, now there's no callback, we can pass whatever function we want into that useClickoutside hook and that is our second custom hook for today. What's fantastic about this is you can move this around your app, you can import it to as many components as you want or you can just copy and paste this into another app. There's no external dependencies other than React and I do this all the time. Greenfield project, I'm like oh! I really love that hook, I go back to the last project that I was working on, I open the file, I copy it and I move it over, it's that easy. I don't even really rely on external libraries that do a lot of this stuff because it's just so simple. Something that isn't simple is state, particularly global state. Global state is highly opinionated. There's a new way to manage global state. Every day it comes out there's a new library. It's kind of noisy. And we're going to be talking a lot about global state here for the last part of this talk. Global state in my opinion is something that's always trying to 1up you. It's because you think you have a handle on it and then your requirements change and everything changes in your app and it kind of pulls the rug out from underneath you. This is usually how global state starts and you're like Wow! This is so simple. I have a hook, it's going to return some global state theoretically and then for some reason it can quickly turn into something like this, not to name names or anything, but this is something I would try and avoid. I don't want to have to go to these links to use global state. So, I'm going to take you on a little bit of a journey of finding global state today and it's going to be so introspective and enlightening that maybe you'll want to actually handle your own global state after today. Let's start by creating a global store. We can use React context to pass values down our React tree, not have to pass them through props. We can start with a store provider that's just a wrapper around context that will set up some state with a store and a setStore function right there that we can pass down through our context value and then we'll have a useStore custom hook that just wraps around that use context and what that does is now inside of our app we can declare some initial state, pass that initial state into our store provider and now that state is going to be available to all the components inside of it, including the little Todos component down there. And inside of our components we can consume the store pretty easily by just calling useStore which gives us the current state of the store and a way to update it which right now is just setState. setState is kind of boring though and it's prone to break and I don't really feel good about manipulating the store directly from a component. So, what if we took it a step further to useReducer. useReducer is really pretty simple to use in my opinion. Just create a reducer function here. We can do it in our app. We can pass that reducer function into our store provider we created and instead of useState now we can just useReducer, pass the reducer through the initial state and instead of a setState function to directly modify the store now we have a dispatcher we can use. So, in our components instead of importing the setState, we get the dispatch and now we can dispatch actions and have a little bit more confidence about the actions that are taking place inside of our app, especially even for components like an individual to do where we're not necessarily reading from the store but just dispatching, we can still import the store and get a hold of dispatcher to send off some events. This pattern is really simple. It will probably get you pretty far, but at the end of the day it might bring up questions about unnecessary renders. You know, I'm rerendering every component in my app, every single time that the state changes even if I'm only dispatching. Well, it turns out we can use a concept called multiple contexts to aid this a little bit. A Multi-Context Global Store was first introduced to me by my friend Kent C. Dodds and he's made it popular through a couple of blog posts which are just fantastic and it describes creating two separate contexts for your store and your dispatcher and sending them both down the line and creating another useDispatch custom hook there. So, now inside of our components if we want the store, we use the useStore hook, and if we want the dispatcher, we use the useDispatch hook. This makes it so that in components that are only using the dispatcher they're not going to get updated every single time the store updates and that will get rid of a lot of weird unnecessary renders around that scenario. Let's take it a step further. What about a single store? It is probably not going to scale for very long if you have a lot of global state in your application, you don't want every single component updating when this state that you're consuming updates that global state. So, we can take it a step further with multiple stores. We can take all of that logic that we had to create our global store and just wrap it into a function and we can create as many of these global stores as we want by passing them in a reducer in initial state. So, if we wanted to do a store, we just pass it the todosReducer, it gives us back the provider and a couple of hooks to consume it and we're off to the races. We come into our app, we provide our Todos to the entire app and I want some global state to manage all of my menus being open and stuff like that. So, I'll make a layout store as well and I'll wrap that around my app and then inside of our component like our Menu that we wanted to click outside and have it change around, we can hook it up to that useLayoutStore by both subscribing to the store to see if it's open and the dispatcher to make sure that we can toggle it close when we click outside of it. The next step in our journey is persistence. Now our global store isn't really persisting anything. This is extremely important because your users are going to reload the page and everything's going to disappear. So, let's throw in some local storage. Local storage is pretty easy to use especially if you're using a reducer. So, right here we can instantiate our reducer initial value from local storage, and also every time a reducer runs, we can save the resulting state into local storage using a key and now every single time that your users reload the page they're going to get the last state that they were left with. I know this is where you guys want to be and I'm working hard to get you there. Just to make sure that everybody is awake. Like that! I got some t-shirts. All right. Moving on. What about Remote Data Persistence? So, local storage is great but eventually users are going to want some remote data persistence, they're going to want to store their data on a server, they're going to want things to sync between their devices. They're so entitled. So, let's talk about Server State. Now I say server state because I believe that it is truly different from global state but it is the same in spirit. Server state introduces some interesting challenges around moving from synchronous APIs to asynchronous APIs. We're no longer interacting with a store or a dispatcher, we are interacting with fetching assets and sending mutations or queries to the server and expecting them to do things, and along with that we're going to notice that a lot of our components where we'd previously wired up things like dispatch and the store that kind of becomes a little bit of a question mark now, where are we getting those things? Well, turns out that our components are really great places to compose business logic and user interface together and this can be so easy that sometimes it's a detriment. We take things like local state markup, UI events, styles, things that I feel are meant to be in components and we kind of mix them together with all this state and these external utilities or maybe expensive computation or side effects that are happening on servers, and we take all that and we smoosh it together inside of our components and sometimes it can make them hard to reason about and I'd like to propose a new pattern today where we need to remember that custom hooks are free now. There's no cost of adding a new custom hook other than the cost of abstraction. You can ask Kent C. Dodds how he feels about the cost of abstraction, he talks about it a lot. But today, I'd like to introduce a new layer of abstraction to our journey, they're just custom hooks. There's really nothing to say other than that and they're going to proxy all of our business logic between our components and the external services and the external features that we want to integrate with them. So, in this case, we're going to take our Todos component, we're going to write a useTodos custom hook that's going to do all the heavy lifting for us between our data. What would that look like? We could just start with a useTodo, we can import useTodos into our component and just expect that it returns Todos and that would be enough for now. This useTodos hook could do whatever we wanted to as long as it returns Todos, that's all we care about. The implementation details are hidden from the component, it doesn't care and it shouldn't. So, if we wanted to implement our existing in-memory store, we could just import our store and return the Todos from it and we would be fine, but remember when you talk about asynchronous state and state coming from the server, that changes a lot of things, especially if we're not talking about suspense which is a whole other talk, but let's assume that we're not using suspense and this is what something like that would look like. We know that it's going to be loading at some point, we know there could be an error or we know we could have Todos. And as long as we conform to this function signature, we can actually do a lot with this custom hook, this useTodos custom hook that we just made. We could use an effect and like a tool like axios to communicate with a ref server that would load our Todos for us and we can manage all of the loading state and everything ourselves or we could use a very common hook called a usePromise that will let us pass a function that returns a promise and handle all of that isLoading and error state for us. Again, as long as we're returning the Todos, the loading state and the error, any component that's consuming Todos isn't really going to care and we could do the same thing, take this pattern and apply the same pattern to anything doing a mutation. So, if we had a todo component that was going to toggle something, we could have that useToggle todo custom hook take care of that for us. So, instead of importing some REST API or axios directly inside of our component, we import useToggleTodo, we call it and it'll give us a function, and you know what? Instead of just giving us function, let's assume it's going to be asynchronous, it'll give us a function and the loading state and error state as well and again that unlocks us the ability to do whatever we want in that useToggleTodo hook. We can do whatever we want as long as we're returning a function that the component can call and an isLoading and error state. So, we could use our in-memory store again and just hook it right up to our Todo store, isLoading and error is false and null all the time if we're doing this or we can go into the REST paradigm and just use axios to issue off some type of a post command to our server to toggle this Todo, but this can get pretty confusing very quickly if you're managing all these side effects, especially when we start thinking about things like caching. What happens when we're using these custom hooks multiple times in our app if we need Todos in multiple places, are they separate copies? Are we reduplicating things? Are we creating race conditions? And if we're caching, when are we refetching that data? There are so many questions that happen along there. Well, we don't have to stop there. I used Todos custom hook and it doesn't have to communicate directly with our server, it could use other hooks as well. There's a library created called React query that helps with scenarios like this. You can use React query to give it a function that returns a promise, it'll handle caching and invalidation and make sure that you're not duplicating Todos across your entire app even if you use it multiple times. I mean you'll notice that the really cool part is that our return signature is exactly the same. So, we just moved from this weird funky promise based way of fetching our data to something like React query and all of our components that use the useTodos hook didn't have to change at all. It's just an implementation detail. Same thing we could do for the mutations. We could add in React query for the useToggleTodo and in fact React query handles a lot of the invalidation for mutations. So, running this mutation would invalidate all of the other queries on the page getting Todos and it would cause all of them to update. It's not very unsimilar to tools like Apollo. And I don't use GraphQL a whole lot, but when I do, I do enjoy using Apollo. And what would it take to switch to something like Apollo? Well, we could import Apollo and make a query, use the useQuery hook from Apollo and again we're just returning Todos as loading an error, we just moved from React query to Apollo, we didn't have to change anything in our entire app. So, same thing with causing a mutation, as long as we're returning that ToggleTodo and isLoading error, again our mutations don't have to change. So, we were able to just in the last five minutes, well, go from in-memory management of our data to promises to React query and Apollo, we can probably even move to Relay if we wanted, all because we're using this pattern of creating your own custom hooks to abstract our own business logic in our apps away from our components to make them smarter and to make them more reliable. This pattern to me is extremely powerful. I see basically all the time in my applications, all my components are communicating with custom hooks to handle most of their business logic which are then reaching out to possibly even more custom hooks or external APIs or whatever. This creates a really nice encapsulation around what is user interface, what is business logic and what are the services I'm trying to use in my applications. Thinking from this perspective you can start to imagine so many great custom hooks that you would want to use inside of your apps, but maybe not something that would be shareable outside of your app. It's okay if a custom hook is proprietary to something you're working on if it makes you more productive. So, these custom hooks and the idea of custom hooks is so powerful, I take things to the extreme and I create tons of open-source libraries built on custom hooks. Those four are just a few of them. React table is in fact just a hook, it actually doesn't render anything for you, it just is a hook that gives you everything you need to render your own table, it's a really interesting concept, but my hope and invitation to all of you today is to look at your hooks in your apps and think of ways that you can make yourself more agile, abstract your business logic into your custom hooks and make your components dumber thereby making them smarter. So, be sure to check me out at GitHub, Twitter, YouTube. Come talk to me. I'd love to chat. And can't forget nozzle.io, that's my company that I started back in Utah. We build marketing software that helps agencies know where they rank in Google. So, thanks. [Applause]
Info
Channel: JSConf
Views: 39,355
Rating: 4.9762611 out of 5
Keywords:
Id: J-g9ZJha8FE
Channel Id: undefined
Length: 28min 30sec (1710 seconds)
Published: Sat Mar 28 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.