React hooks in callback
using hooks in callbacks will be helpful in some cases to filter out noisy hooks and in other cases to avoid defining useless and repetitive hooks in our components just to pass their values to those callbacks
so, This package will help us
- to filter out unwanted hooks re-renders (eg: react context related hooks, hooks with timeout or time interval, etc...).
- to have a simplified version of async actions (that will allow us to have a really nice alternative to the redux-thunk approach)
Install
npm i -S react-hooks-in-callback
useHooksInCallback
import { useHooksInCallback } from "react-hooks-in-callback";
import { useYourCustomHook } from "./my-custom-hooks";
... // here is the component body
const [HooksWrapper, getHookState, subscribeToHookState] = useHooksInCallback();
// HooksWrapper: is a React component where your hooks will be mounted.
// getHookState: an helper that let you get the hook state in an async way.
// subscribeToHookState: same as getHookState, but designed to work with useEffect
...
// you can either use getHookState or subscribeToHookState, depending on your case
...
return (
<div>
{/* useYourCustomHook will be mounted in HooksWrapper */}
<HooksWrapper />
<button onClick={async () => {
// mount useYourCustomHook and wait for its state to be resolved.
const hookState = await getHookState(useYourCustomHook);
// after being resolved, useYourCustomHook is directly unmounted.
}}/>
</div>
)
Formik example
Imagine to have a list of fields where each field component uses useFormikContext, just to set the field value on click event. the scenario is the following one:
const Field = (name: string) => {
const formik = useFormikContext();
...
return (
<div>
<button onClick={() => formik.setFieldValue(name, newFieldValue)}/>
<div/>
)
}
The issue here is the re-render noise introduced by the formik context. Everytime a field will be updated, all the other fields will re-render since they are using the same react context. This will lead to a bad performance.
Check the formik with context's re-render noise example here
We can solve that issue if we can take the useFormikContext out of the Field component and get its state only when there is a click event. This is what we are going to do by using useHooksInCallback.
const Field = (name: string) => {
const [HooksWrapper, getHookState] = useHooksInCallback();
...
return (
<div>
<HooksWrapper /> {/* added! */}
<button onClick={async () => {
// formik context will be used only once in this callback
const formik = await getHookState(useFormikContext);
formik.setFieldValue(name, newFieldValue)
}}/>
<div/>
)
}
Check the formik with hooks-in-callback example here
useActionUtils
A place where we usually use hooks states is in a redux-thunk action. the reason to use the react-hooks-in-callback approach instead is because it brings some benefits.
- your action has only one callback layer: not a curry function like in redux-thunk approach
- hooks based params are not defined anymore in the component but directly in the action callback: just think if we have a login action and we need to change it in a way to push /login on start and /home on success, we need to have history as parameter and define const history = useHistory() in every component where our login action will be used.
- filtering unwanted re-renders as we saw previously
usage
first of all, we need to create utilities for our async actions
import { createActionsPackage } from 'react-hooks-in-callback'
export const actionsPkg = createActionsPackage()
// actionsPkg: { HooksWrapper, getHookState, subscribeToHookState }
// HooksWrapper => Component to be mounted at the top level, directly under all used hooks contexts providers
// getHookState => get your hook state in an async way in your action
// subscribeToHookState => subscribe to hooks state changes (probably you don't need it for your actions)
then we need to mount the HooksWrapper to process our hooks states
import { actionsPkg } from './configs'
const { HooksWrapper } = actionsPkg
const MyRootComponent = (props) => {
return (
<Provider store={store}>
<Router>
{/*
HooksWrapper is where action utils hooks will be mounted
so it should be under the providers tree and before the components where the actions will be called.
*/}
<HooksWrapper />
{props.children}
</Router>
</Provider>
)
}
now we can define a custom hook for our actions
export const useActionUtils = () => {
const { dispatch, getState } = useStore()
const history = useHistory()
return { dispatch, getState, history }
}
and use it like follows
import { actionsPkg, api } from './configs'
import { useActionUtils } from './hooks'
const { getHookState } = actionsPkg
const login = async (userId: string) => {
// here we will mount useActionUtils in the HooksWrapper component and wait for its state to be resolved.
const { dispatch, history, getState } = await getHookState(useActionUtils)
try {
history.push('/login')
dispatch({ type: 'LoginStart' })
const { data: token } = await api.login(userId)
const { data: users } = await api.getUsers(token)
dispatch({ type: 'LoginSuccess', payload: users })
history.push('/home')
// just to check if everything is fine, you can log your redux state here
// const storeState = getState();
// console.log(storeState)
} catch (error) {
dispatch({ type: 'LoginError', payload: error })
}
}
As we can notice in our action, the only one parameter is userId. every other parameters related to hooks are defined directly in useActionUtils and every change depending on it will be done only in it and won't affect our components.
if it was a redux-thunk action, the synthax would be more complex, we can see the difference bellow.
// redux-thunk action synthax
const login = (userId: string, history: History) => {
// hooks values/states should be passed as action params like we passed history in this example
return async (dispatch, getState, config: Config) => {
// your logic goes here
}
}
// react-hooks-in-callback action synthax
const login = async (userId: string) => {
// hooks values/states are defined directly in the action body
// your logic goes here
}
the last step now is to use everything in a component
import { login } from './actions'
const App = () => {
useEffect(() => {
login('admin') // don't need to dispatch or to pass history
}, [])
return <div>...</div>
}
just to compare both approaches, if we used a redux-thunk way instead, we had to define dispatch and history in our components to dispatch the login action and pass history as parameter
import { login } from './actions'
const App = () => {
// if we used a redux-thunk action we should need dispatch and history in our component like bellow
const dispatch = useDispatch() // +++++
const history = useHistory() // +++++
useEffect(() => {
dispatch(
// we need to dispatch an action passing also history
login('admin', history)
)
}, [dispatch])
return <div>...</div>
}
You can find the redux sandbox example here
Try it out!
Advanced
Waiting for a specific state before resolving the getHookState
sometimes your expected hook state is not the first provided one and you should wait for a specific state before resolving the getHookState value.
For example this following hooks returns the total number of divs in the DOM, but initially returns undefined.
const useDivCount = () => {.
const [state, setState] = useState<number>();
useEffect(() => {
const divs = document.querySelectorAll("div");
setState(divs?.length || 0);
}, []);
return state; // undefined | number
};
So in this case what we want to do is to skip the undefined value and wait for the number value. to do so, we just need to implement it by adding a suspender as a second argument to the getHookState
const hookState = await getHookState(useDivCount, (state, utils) => {
if (state !== undefined) {
utils.resolve() // this will resolve the current state
return
}
if (utils.isBeforeUnmount) {
// this should not happen normally, but if it happens and
// you did not resolve the getHookState and some how you are unmounting the component
// you should do something to not keep this promise in pending state
// utils.resolve or use utils.reject
}
})
you can also subscribe to state changes in useEffect using subscribeToHookState
const [HooksWrapper, , subscribeToHookState] = useHooksInCallback()
useEffect(() => {
const subscription = subscribeToHookState(useDivCount, (
error,
data /*{ state: S; isBeforeUnmount: boolean }*/
) => {
if (error) {
// console.error(error.message);
return
}
// subscription logic goes here
const { state, isBeforeUnmount } = data
})
return subscription.unsubscribe
}, [])
Find an advanced example here
see also
License
MIT © https://github.com/fernandoem88/react-hooks-in-callback