@tanner linsley
You
JSconf
Hook Tutorials
Videos
Courses
JS
Devs
You
Hooks
Tutorials
Talks
Courses
React Hooks Basics
π Functions π
Classes
Classes
class ProfilePage extends React.Component {
state = {
count: 0,
}
render() {
return (
<>
<p>Count: {count}</p>
<button onClick={() => this.setState({ count: count + 1 })}>
Increment
</button>
</>
)
}
}
State π€¨ this.state
function Example() {
const [count, setCount] = React.useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</>
);
}
useState πΒ or useReducer
class ProfilePage extends React.Component {
state = {
user: null,
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const user = await fetchUser(id);
this.setState({ user });
}
render() {
...
}
}
Class LifecyclesΒ π change detection
function ProfilePage ({ id }) {
const [user, setUser] = React.useState(null)
React.useEffect(() => {
const fetchData = async (id) => {
const user = await fetchUser(id);
setUser(user)
}
fetchData(id)
}, [id])
...
}
Side EffectsΒ π₯³ Synchronization
function DataGrid ({ data, columns }) {
const rows = React.useMemo(() => {
// expensive function
return computeRows({ data, columns })
}, [data, columns])
...
}
MemoizationΒ πΒ Computed State
function App() {
const stuff = useCustomHook()
return ...
}
π Custom HooksΒ π
function App() {
return (
<Auth>
{({ jwt }) => (
<User jwt={jwt}>
{({ userId }) => (
<Filters>
{({ filter }) => (
<Todos userId={userId} filter={activeFilter}>
{({ todos, isLoading, error }) => (
<div>
{isLoading
? 'Loading...'
: error
? 'Error!'
: todos.map(todo => <Todo todo={todo} />)}
</div>
)}
</Todos>
)}
</Filters>
)}
</User>
)}
</Auth>
)
}
HOCs, Render Props...
function App() {
const jwt = useAuth()
const { userID } = useUser({ jwt })
const { activeFilter } = useFilters()
const { todos, isLoading, error } = useTodos({
userId,
filter: activeFilter
})
return (
<div>
{isLoading
? 'Loading...'
: error
? 'Error!'
: todos.map(todo => <Todo todo={todo} />)}
</div>
)
}
π Custom HooksΒ π
Affordances
π₯
Migrations
New Projects
Favorite "use" Cases
???
Dark Mode
function App () {
const [isDark, setIsDark] = React.useState(false)
const theme = isDark ? themes.dark : themes.light
return (
<ThemeProvider theme={theme}>
...
</ThemeProvider>
)
}
const matchDark = '(prefers-color-scheme: dark)'
function App() {
const [isDark, setIsDark] = React.useState(
() => window.matchMedia(matchDark).matches
)
...
}
window.matchMedia
...
React.useEffect(() => {
const matcher = window.matchMedia(matchDark)
const onChange = ({ matches }) => setIsDark(matches)
matcher.addListener(onChange)
return () => {
matcher.removeListener(onChange)
}
}, [setIsDark])
...
useEffect() + Media Watcher
const matchDark = '(prefers-color-scheme: dark)'
function App() {
const [isDark, setIsDark] = React.useState(
() => window.matchMedia && window.matchMedia(matchDark).matches
)
React.useEffect(() => {
const matcher = window.matchMedia(matchDark)
const onChange = ({ matches }) => setIsDark(matches)
matcher.addListener(onChange)
return () => {
matcher.removeListener(onChange)
}
}, [setIsDark])
const theme = isDark ? themes.dark : themes.light
return <ThemeProvider theme={theme}>...</ThemeProvider>
}
const matchDark = '(prefers-color-scheme: dark)'
function useDarkMode() {
const [isDark, setIsDark] = React.useState(
() => window.matchMedia && window.matchMedia(matchDark).matches
)
React.useEffect(() => {
const matcher = window.matchMedia(matchDark)
const onChange = ({ matches }) => setIsDark(matches)
matcher.addListener(onChange)
return () => {
matcher.removeListener(onChange)
}
}, [setIsDark])
return isDark
}
useDarkMode()
import useDarkMode from './useDarkMode'
function App() {
const theme = useDarkMode()
? themes.dark
: themes.light
return (
<ThemeProvider theme={theme}>
...
</ThemeProvider>
)
}
"use"-ing it!
function Menu () {
return (
<div>
...
</div>
)
}
Click Outside
import useClickOutside from './useClickOutside'
function Menu() {
const menuRef = React.useRef()
const onClickOutside = () => {
console.log('Clicked Outside!')
}
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
Too soon? π
function useClickOutside(elRef, callback) {
...
}
function useClickOutside(elRef, callback) {
React.useEffect(() => {
const handleClickOutside = e => {
if (!elRef?.current?.contains(e.target) && callback)
callback(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callback, elRef])
}
import useClickOutside from './useClickOutside'
function Menu () {
const menuRef = React.useRef()
const onClickOutside = () => {
console.log('Clicked Outside!')
}
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
Something is π -y
eslint-plugin-react-hooks
React.useEffect(() => {
...
}, [])
React.useEffect(() => {
...
}, [foo, bar])
import useClickOutside from './useClickOutside'
function Menu () {
const menuRef = React.useRef()
const onClickOutside = () => {
console.log('Clicked Outside!')
}
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
React.useCallback
function useClickOutside(elRef, callback) {
React.useEffect(() => {
const handleClickOutside = e => {
if (elRef?.current?.contains(e.target) && callback)
callback(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callback, elRef])
}
import useClickOutside from './useClickOutside'
function Menu () {
const menuRef = React.useRef()
const onClickOutside = React.useCallback(() => {
console.log('Clicked Outside!')
}, [])
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
function useClickOutside(elRef, callback) {
React.useEffect(() => {
const handleClickOutside = e => {
if (elRef?.current?.contains(e.target) && callback)
callback(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callback, elRef])
}
function useClickOutside(elRef, callback) {
React.useEffect(() => {
const handleClickOutside = e => {
if (elRef?.current?.contains(e.target) && callback)
callback(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callback, elRef])
}
function useClickOutside(elRef, callback) {
const callbackRef = React.useRef()
callbackRef.current = callback
React.useEffect(() => {
const handleClickOutside = e => {
if (elRef?.current?.contains(e.target) && callbackRef.current)
callbackRef.current(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callbackRef, elRef])
}
import useClickOutside from './useClickOutside'
function Menu () {
const menuRef = React.useRef()
const onClickOutside = () => {
console.log('Clicked Outside!')
}
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
Look Ma! No useCallback!
function useClickOutside(elRef, callback) {
const callbackRef = React.useRef()
callbackRef.current = callback
React.useEffect(() => {
const handleClickOutside = e => {
if (elRef?.current?.contains(e.target) && callbackRef.current)
callbackRef.current(e)
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
}, [callbackRef, elRef])
}
π useClickOutside.js π
State
Global
Global State
You
function Todos () {
const { todos } = useStore()
}
π
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
function Todos () {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
π³ WAT π³
"Finding Global State"
const context = React.createContext()
export const StoreProvider = ({ children, initialState = {} }) => {
const [store, setStore] = React.useState(() => initialState)
const contextValue = React.useMemo(() => [store, setStore], [store])
return (
<context.Provider value={contextValue}>
{children}
</context.Provider>
)
}
export default function useStore() {
return React.useContext(context)
}
Creating a Global Store
import { StoreProvider } from './useStore'
const initialState = {
todos: []
}
function App () {
return (
<StoreProvider initialState={initialState}>
<Todos />
</StoreProvider>
)
}
<App>
import useStore from './useStore'
function Todos () {
const [{ todos }, setStore] = useStore()
const addTodo () => {
setStore(old => ({
...old,
todos: [...old.todos, { name: 'New Todo' }]
})
}
...
}
Consuming the store
π€Β setState?
πΒ useReducer
import { StoreProvider } from './useStore'
const initialState = {
todos: []
}
const reducer = (state, action) => {
switch (action.type) {
case 'addTodo':
return {
...state,
todos: [
...state.todos,
action.todo
]
}
default:
throw new Error('Unknown action!', action);
}
}
function App () {
return (
<StoreProvider reducer={reducer} initialState={initialState}>
<Todos />
</StoreProvider
)
}
<App>
export function StoreProvider ({
children,
reducer,
initialState = {}
}) {
const [store, dispatch] = React.useReducer(
reducer,
initialState
)
const contextValue = React.useMemo(
() => [store, dispatch],
[store, dispatch]
)
return (
<context.Provider value={contextValue}>
{children}
</context.Provider>
)
}
Global Store
import useStore from './useStore'
function Todos () {
const [{ todos }, dispatch] = useStore()
const addTodo () => dispatch({
type: 'addTodo',
todo: {
name: 'New Todo'
}
})
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</div>
)
}
<Todos>
import useStore from './useStore'
function Todo ({ todo }) {
const [, dispatch] = useStore()
const handleClick = () => {
dispatch({
type: 'toggleTodo',
todoId: todo.id
})
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
<Todo>
π€Β unnecessary renders?
πΒ multiple contexts
πΒ multiple contexts
Multi-Context Global Store
const storeContext = React.createContext()
const dispatchContext = React.createContext()
export const StoreProvider = ({ children, reducer, initialState = {} }) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
return (
<dispatchContext.Provider value={dispatch}>
<storeContext.Provider value={store}>
{children}
</storeContext.Provider>
</dispatchContext.Provider>
)
}
export function useStore() {
return React.useContext(storeContext)
}
export function useDispatch() {
return React.useContext(dispatchContext)
}
Kent C. Dodds
import { useStore } from './useStore'
import Todo from './Todo'
function Todos () {
const { todos } = useStore()
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</div>
)
}
<Todos>
import { useDispatch } from './useStore'
function Todo ({ todo }) {
const dispatch = useDispatch()
const handleClick = () => {
dispatch({ type: 'toggleTodo', todoId: todo.id })
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
<Todo>
π€Β single store?
πΒ multiple stores
Multiple Stores w/ makeStore()
export default function makeStore (reducer, initialState) {
const dispatchContext = React.createContext()
const storeContext = React.createContext()
const StoreProvider = ({ children }) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
return (
<dispatchContext.Provider value={dispatch}>
<storeContext.Provider value={store}>
{children}
</storeContext.Provider>
</dispatchContext.Provider>
)
}
function useDispatch() {
return React.useContext(dispatchContext)
}
function useStore() {
return React.useContext(storeContext)
}
return [StoreProvider, useDispatch, useStore]
}
Todos Store
import makeStore from './makeStore'
const todosReducer = (state, action) => {...}
const [
TodosProvider,
useTodos,
useTodosDispatch
] = makeStore(todosReducer, [])
export { TodosProvider, useTodos, useTodosDispatch }
import { TodosProvider } from './useTodosStore'
function App () {
return (
<TodosProvider>
<Todos />
</TodosProvider
)
}
<App>
import { TodosProvider } from './useTodosStore'
import { LayoutProvider } from './useLayoutStore'
function App () {
return (
<TodosProvider>
<LayoutProvider>
<Todos />
</LayoutProvider>
</TodosProvider
)
}
<App>
<Menu>
import useClickOutside from './useClickOutside'
function Menu () {
const menuRef = React.useRef()
const onClickOutside = () => {
console.log('Clicked Outside!')
}
useClickOutside(menuRef, onClickOutside)
return (
<div ref={menuRef}>
...
</div>
)
}
import useClickOutside from './useClickOutside'
import { useLayoutStore, useDispatch } from './useLayoutStore'
function App() {
const { menuIsOpen } = useStore()
const dispatch = useDispatch()
const elRef = React.useRef()
useClickOutside(elRef, () =>
dispatch({ type: 'menuToggled', isOpen: false })
)
return (
<div ref={elRef} onClick={handleOpen}>
...
</div>
)
}
<Menu>
π€ No persistance?
π Local Storage
Global Store + localStorage
export default function makeStore(userReducer, initialState, key) {
const dispatchContext = React.createContext()
const storeContext = React.createContext()
try {
initialState = JSON.parse(localStorage.getItem(key)) || initialState
} catch {}
const reducer = (state, action) => {
const newState = userReducer(state, action)
localStorage.setItem(key, JSON.stringify(newState))
return newState
}
const StoreProvider = ({ children }) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
return (
<dispatchContext.Provider value={dispatch}>
<storeContext.Provider value={store}>{children}</storeContext.Provider>
</dispatchContext.Provider>
)
}
function useDispatch() {
return React.useContext(dispatchContext)
}
function useStore() {
return React.useContext(storeContext)
}
return [StoreProvider, useDispatch, useStore]
}
π
π³
πΒ Remote Data Persistance?
π Server State
π€
syncΒ Β Β Β async
dispatchΒ Β Β mutation
storeΒ Β Β server
import { useDispatch } from './useTodosStore'
function Todo ({ todo }) {
const dispatch = useDispatch()
const handleClick = () => {
dispatch({ type: 'toggleTodo', todoId: todo.id })
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
import { useTodos } from './useTodosStore'
function Todos () {
const todos = useTodos()
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</div>
)
}
import { ??? } from './???'
function Todo ({ todo }) {
const toggleTodo = ???()
const handleClick = () => {
toggleTodo(todo.id)
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
import { ??? } from './???'
function Todos () {
const todos = ???()
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</div>
)
}
Business Logic
User Interface
Components
Component
Component
Store
useBusinessLogic()
<Component />
<Todos />
Store / Server
useTodos()
Custom Hooks are π!!!
import useTodos from './useTodos'
function Todos () {
const todos = useTodos()
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</div>
)
}
Business Logic Hooks
useTodos.js
export function useTodos() {
return whateverWeWant()
}
import { useTodosStore } from './useTodosStore'
export function useTodos() {
const todos = useTodosStore()
return todos
}
Using the in-memory store
import { useTodosStore } from './useTodosStore'
export function useTodos() {
const todos = useTodosStore()
return {
todos,
isLoading: false,
error: null
}
}
Assume async data
export function useTodos() {
const [todos, setTodos] = React.useState([])
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const fetchTodos = React.useCallback(async () => {
setIsLoading(true)
try {
const { data: todos } = await axios.get('/todos')
setTodos(todos)
} catch (err) {
setError(err)
}
setIsLoading(false)
}, [setIsLoading, setTodos, setError])
React.useEffect(() => {
fetchTodos()
}, [fetchTodos])
return {
todos,
isLoading,
error
}
}
useEffect() + axios
import usePromise from './usePromise'
export function useTodos() {
const getTodos = React.useCallback(async () => {
const { data } = await axios.get('/todos')
return data
}, [])
const { data: todos, isLoading, error } = usePromise(getTodos)
return {
todos,
isLoading,
error,
}
}
usePromise() + REST
Component
Component
REST GET
useTodos()
<Todos />
<Todo />
REST POST
useToggleTodo()
<Todo>
import useToggleTodo from './useToggleTodo'
function Todo ({ todo }) {
const toggleTodo = useToggleTodo()
const handleClick = () => {
toggleTodo(todo.id)
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
<Todo>
import useToggleTodo from './useToggleTodo'
function Todo ({ todo }) {
const [toggleTodo, { isLoading, error }] = useToggleTodo()
const handleClick = () => {
toggleTodo(todo.id)
}
return (
<div onClick={handleClick}>{todo.name}</div>
)
}
useToggleTodo()
export function useToggleTodo() {
...
return [toggleTodo, { isLoading, error }]
}
Using the in-memory store
import { useDispatch } from './todosStore'
export function useToggleTodo() {
const dispatch = useDispatch()
const toggleTodo = React.useCallback(id => {
dispatch({
type: 'toggle_todo',
todoId: id,
})
})
return [toggleTodo, { isLoading: false, error: null }]
}
export function useToggleTodo() {
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const toggleTodo = React.useCallback(
async todoId => {
setIsLoading(true)
try {
const { data } = await axios.get(
`https://example.com/todos/${todoId}/toggle`
)
return data
} catch (err) {
setError(err)
}
setIsLoading(false)
},
[setIsLoading, setError]
)
return [toggleTodo, { isLoading, error }]
}
Using REST
π€
<Todos />
Store / Server
useTodos()
React Query
import { useQuery } from 'react-query'
const fetchTodos = () =>
axios.get('https://example.com/todos').then(res => res.data)
export function useTodos() {
const { data: todos, isLoading, error } = useQuery('todos', fetchTodos)
return { todos, isLoading, error }
}
useTodos() + React Query
π€
<Todos />
Store / Server
useToggleTodo()
React Query
<Todo>
import useToggleTodo from './useToggleTodo'
function Todo({ todo }) {
const [toggleTodo, { isLoading, error }] = useToggleTodo()
const handleClick = () => toggleTodo(todo.id)
return isLoading ? (
'Toggling...'
) : error ? (
'Error!'
) : (
<div onClick={handleClick}>{todo.name}</div>
)
}
import { useMutation } from 'react-query'
const getToggleTodoById = todoId =>
axios.get(`https://example.com/todos/${todoId}/toggle`)
.then(res => res.data)
export function useToggleTodo() {
return useMutation(getToggleTodoById, {
refetchQueries: ['todos']
})
}
useToggleTodo() + React Query
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
const GET_TODOS = gql`
query getTodos {
...
}
`;
export function useTodos() {
const {
data: todos,
loading: isLoading,
error
} = useQuery(GET_TODOS);
return { todos, isLoading, error }
}
useTodos() + Apollo
import { useMutation } from '@apollo/react-hooks'
import gql from 'graphql-tag'
const TOGGLE_TODO = gql`
mutation ToggleTodo($todoId: Id!) {
toggleTodo(id: $todoId) {
id
done
}
}
`
export function useToggleTodo() {
const [toggleTodo, { loading: isLoading, error }] = useMutation(TOGGLE_TODO, {
refetchQueries: ['getTodos']
})
return [toggleTodo, { isLoading, error }]
}
useToggleTodo() + Apollo
π²
π
import useTodos from './useTodos'
import useToggleTodo from './useToggleTodo'
...
useTodos()
useToggleTodos()
Component
Component
Component
Custom Hook
Custom Hook
Store
API
Custom Hook
Component
Component
Component
UI
Business & UI Logic
Data
Services
Integrations
Utilities
usePermissions
useDebounce
useToast
usePagination
useLoading
useSubscription
useAuth
useModal
useClipboard
useForm
useTooltip
useUser
github.com
youtube.com
twitter.com
/ tannerlinsley