
@tanner linsley




You
JSconf
in
Ho
oks!
Custom
React
React
React Hook
Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React
React
React Hook
Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React Hooks!
React

Hook Tutorials
Videos
Courses
JS
Devs
You
Hooks
Tutorials
Talks
Courses
not
your
Mother's
React Hook
Tutorial
Fast
Code ++
Fun +++++
"use" +++++++
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 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]) ... }
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]) ... }
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 ... }
- useState()
- useReducer()
- useEffect()
- useMemo()
- useCallback()
๐ 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 ๐
- ๐ทโโ๏ธ Build-your-own!
- ๐ฆ Portable / Sharable Logic
- ๐ง Component-Aware Abstractions
- ๐ Rapid Iteration
Affordances
๐ฅ
Components
==
User Interface
Hooks
==
Business Logic
Migrations
New Projects
- ๐จ Portable UI Utilities
- ๐ Global State
- ๐ Server State
- ๐ผ Business Logic
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> ) }
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 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 }
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> ) }
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' function Menu() { const menuRef = React.useRef() const onClickOutside = () => { console.log('Clicked Outside!') } useClickOutside(menuRef, onClickOutside) return ( <div ref={menuRef}> ... </div> ) }
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' 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]) }
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) { 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]) }
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) }
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) }
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' }] }) } ... }
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 ) }
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> ) }
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) }
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] }
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 ) }
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
- Shared State
- Utilities
- Computation
- Etc...
- Local State
- Markup
- UI Events
- Styles
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, } }
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 }] }
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 }] }
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 }] }
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
๐ค
- Caching?
- Multiple useTodos()?
- Race Conditions?
- Stale Requests?
- When do we refetch?
<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 } }
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
๐ค
- Server Side-Effects?
- Stale useTodos()?
<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 } }
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 } }
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 }] }
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 }] }
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
๐ฒ
- In-Memory
- Promises
- React Query
- 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
Custom Hooks in React: The ultimate UI abstraction layer you're missing out on
By Tanner Linsley
Custom Hooks in React: The ultimate UI abstraction layer you're missing out on
The slides for my talk at JS Conf Hawaii 2020
- 3,128