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]