21
loading...
This website collects cookies to deliver better user experience
BookInfo
component in the last part, also explained what these components are and what they are doing. If you haven't still read that, go check that out first, here.import * as React from 'react'
import {
fetchBook,
BookInfoFallback,
BookForm,
BookDataView,
ErrorFallback,
} from '../book'
function BookInfo({bookName}) {
const [status, setStatus] = React.useState('idle')
const [book, setBook] = React.useState(null)
const [error, setError] = React.useState(null)
React.useEffect(() => {
if (!bookName) {
return
}
setStatus('pending')
fetchBook(bookName).then(
book => {
setBook(book)
setStatus('resolved')
},
error => {
setError(error)
setStatus('rejected')
},
)
}, [bookName])
if (status === 'idle') {
return 'Submit a book'
} else if (status === 'pending') {
return <BookInfoFallback name={bookName} />
} else if (status === 'rejected') {
return <ErrorFallback error={error}/>
} else if (status === 'resolved') {
return <BookDataView book={book} />
}
throw new Error('This should be impossible')
}
function App() {
const [bookName, setBookName] = React.useState('')
function handleSubmit(newBookName) {
setBookName(newBookName)
}
return (
<div className="book-info-app">
<BookForm bookName={bookName} onSubmit={handleSubmit} />
<hr />
<div className="book-info">
<BookInfo bookName={bookName} />
</div>
</div>
)
}
export default App
BookInfo
component and manage them in our custom hook only, we will let users(users of hooks) pass just a callback method and dependencies and the rest will be managed for them.useAsync
hook looks like now:function useAsync(asyncCallback, dependencies) {
const [state, dispatch] = React.useReducer(asyncReducer, {
status: 'idle',
data: null,
error: null,
})
React.useEffect(() => {
const promise = asyncCallback()
if (!promise) {
return
}
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
}, dependencies)
return state
}
function asyncReducer(state, action) {
switch (action.type) {
case 'pending': {
return {status: 'pending', data: null, error: null}
}
case 'resolved': {
return {status: 'resolved', data: action.data, error: null}
}
case 'rejected': {
return {status: 'rejected', data: null, error: action.error}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
asyncReducer
is declared and defined below it is called. JS feels like magic, not much if you know about Hoisting
, if you don't, check this out. function BookInfo({bookName}) {
const state = useAsync(
() => {
if (!BookName) {
return
}
return fetchBook(BookName)
},
[BookName],
)
const {data: Book, status, error} = state
//rest of the code same as above
dependencies
argument is a valid argument for useEffect
, normally it isn't bad we can just ignore it and move on. But, there’s a better solution.useAsync
, why don’t we just treat the asyncCallback
as a dependency? Any time it changes, we know that we should call it again. The problem is that because it depends on the bookName
which comes from props, it has to be defined within the body of the component, which means that it will be defined on every render which means it will be new every render. Phew, This is where React.useCallback
comes in!useCallback
accepts the first argument as the callback we want to call, the second argument is an array of dependencies which is similar to useEffect
, which controls returned value after re-renders.function BookInfo({bookName}) {
const asyncCallback = React.useCallback(() => {
if (!BookName) {
return
}
return fetchBook(BookName)
}, [BookName])
}
const state = useAsync(asyncCallback)
//rest same
useEffect
and manage their own dependencies.useAsync
hook look like this ://!Notice: we have also allowed users(hook user) to send their own initial state
function useAsync(initialState) {
const [state, dispatch] = React.useReducer(asyncReducer, {
status: 'idle',
data: null,
error: null,
...initialState,
})
const {data, error, status} = state
const run = React.useCallback(promise => {
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
}, [])
return {
error,
status,
data,
run,
}
}
BookInfo
component:function BookInfo({bookName}) {
const {data: book, status, error, run} = useAsync({
status: bookName ? 'pending' : 'idle',
})
React.useEffect(() => {
if (!bookName) {
return
}
run(fetchBook(bookName))
}, [bookName, run])
.
.
.
}
function asyncReducer(state, action) {
switch (action.type) {
case 'pending': {
return {status: 'pending', data: null, error: null}
}
case 'resolved': {
return {status: 'resolved', data: action.data, error: null}
}
case 'rejected': {
return {status: 'rejected', data: null, error: action.error}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
action.type
and manually setting different objects of the state according to it.const asyncReducer = (state, action) => ({...state, ...action})
unmount
but when the request is finally completed, it will call dispatch, but because the component is unmounted
, we’ll get this warning from React:Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
React.useRef
hook, learn more about it here.function useSafeDispatch(dispatch) {
const mountedRef = React.useRef(false)
// to make this even more generic we used the useLayoutEffect hook to
// make sure that we are correctly setting the mountedRef.current immediately
// after React updates the DOM. Check the fig below explaining lifecycle of hooks.
// Even though this effect does not interact
// with the dom another side effect inside a useLayoutEffect which does
// interact with the dom may depend on the value being set
React.useLayoutEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return React.useCallback(
(...args) => (mountedRef.current ? dispatch(...args) : void 0),
[dispatch],
)
}
const dispatch = useSafeDispatch(oldDispatch)
mountedRef.current
to true when component is mounted and false when it is unmounted by running cleanup effects.layoutEffects
are executed way before useEffects
.function useAsync(initialState) {
const initialStateRef = React.useRef({
...defaultInitialState,
...initialState,
})
const [{status, data, error}, unsafeDispatch] = React.useReducer(
(s, a) => ({...s, ...a}),
initialStateRef.current,
)
const dispatch = useSafeDispatch(unsafeDispatch)
const reset = React.useCallback(
() => dispatch(initialStateRef.current),
[dispatch],
)
refs
as they don't change between re-renders.initialState
in a ref and the reset
method sets the state to initialState
upon calling, pretty self-explanatory stuff.useAsync
hook looks like this:function useSafeDispatch(dispatch) {
const mounted = React.useRef(false)
React.useLayoutEffect(() => {
mounted.current = true
return () => (mounted.current = false)
}, [])
return React.useCallback(
(...args) => (mounted.current ? dispatch(...args) : void 0),
[dispatch],
)
}
const defaultInitialState = {status: 'idle', data: null, error: null}
function useAsync(initialState) {
const initialStateRef = React.useRef({
...defaultInitialState,
...initialState,
})
const [{status, data, error}, setState] = React.useReducer(
(s, a) => ({...s, ...a}),
initialStateRef.current,
)
const safeSetState = useSafeDispatch(setState)
const setData = React.useCallback(
data => safeSetState({data, status: 'resolved'}),
[safeSetState],
)
const setError = React.useCallback(
error => safeSetState({error, status: 'rejected'}),
[safeSetState],
)
const reset = React.useCallback(
() => safeSetState(initialStateRef.current),
[safeSetState],
)
const run = React.useCallback(
promise => {
if (!promise || !promise.then) {
throw new Error(
`The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
)
}
safeSetState({status: 'pending'})
return promise.then(
data => {
setData(data)
return data
},
error => {
setError(error)
return Promise.reject(error)
},
)
},
[safeSetState, setData, setError],
)
return {
isIdle: status === 'idle',
isLoading: status === 'pending',
isError: status === 'rejected',
isSuccess: status === 'resolved',
setData,
setError,
error,
status,
data,
run,
reset,
}
}
export {useAsync}
"People need to write or teach what they have learned to remember it."