Skip to content

Instantly share code, notes, and snippets.

@cliffordfajardo
Last active Jan 28, 2021
Embed
What would you like to do?
Managing data and business logic in React with custom hooks + React Query
  1. React Query from the creator

    • Watch https://www.youtube.com/watch?v=seU46c6Jz7E
    • TLDR:
      • use global state sparingly. Ask questions:
      • does this really need to be in a context/local state or can I just fetch it when I need it?
      • Authentication data (storing user) globally is one of the few great global state examples.
      • abstract all business logic in hooks
  2. TLDR: abstract away business logic out of components & put them in hooks, its easier to debug, test & helps keeps component very clean with mostly just markup. - Watch: https://www.youtube.com/watch?v=J-g9ZJha8FE - Extra - https://www.youtube.com/watch?v=63YBCxC9U7Y

  3. react-query Examples

  1. React Query Endorsements & Overall Community Sentiment
// A vanilla react example of what `react-query` is doing under the hood with their `useQuery` & `useMutation` hooks.
// (NOTE: this example is a simplified version of the core "under the hood" mechanism of react-query, I'm not doing request de-duping, caching or the other stuff react-query does for free as well)
import {useState, useCallback } from 'react';
import { IQueryState } from '../hook-query-state.interface';
import BOOK_API, { IBook } from '../../lib/api/bookApi';
export function useBookPost(): IQueryState {
// Moving business logic (filters, data transforms, setting up {error, success, loading} states out of components.
// This code is now alot easier test & mock, whereas keeping this inside the component can get messy really quick
const [data, setData] = useState(null);
const [isIdle, setIsIdle] = useState(true);
const [isError, setIsError] = useState(false);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const postBook = useCallback(async (book: IBook) => {
console.log(`[useBookPost] -- Calling postBook()`)
setIsIdle(false);
setIsLoading(true);
try {
let response = await BOOK_API.post(book);
setData(response);
setIsSuccess(true)
console.log(`[useBookPost] -- {isLoading, data}`, {isLoading, data})
}
catch (error) {
setIsError(true);
setError(error);
}
finally {
setIsLoading(false);
setIsIdle(true);
}
}, [])
return {
data,
error,
isError,
isIdle,
isSuccess,
isLoading,
postBook,
}
}
// A vanilla react example of how what consuming custom hooks following the `react-query` style looks like.
function ReaderPreferences(){
//Destructing and renaming the the values we get back from our hooks
const {data: updatedBook, isLoading: isUpdateBookInLoading, isError: isUpdateBookError, isSuccess: isUpdateBookSuccessful } = useBookPost();
const {data: updatedUser, isLoading: isUpdateUserLoading, isError: isUpdateUserError, isSuccess: isUpdateUserSuccessful} = useUpdateUser();
// Easier to independently manage the lifecycle and state of individual CRUD operations.
// Easier to focus on UI independently & Business Logic Indepdently.
// No falling into the easy trap of sharing of useState variables (ex: loading, error, etc) across different CRUD operation
// Imagine all the `useState` decalrations we would have had to setup at the top of this 😭
if(isUpdateBookInProgress){ return <SOME_HTML_OR_COMPONENT> }
if(isUpdateBookError){ return <SOME_HTML_OR_COMPONENT>}
if(isUpdateBookSuccessful){
// do something with the data
return <SOME_HTML_OR_COMPONENT data={updatedBook}>
}
if(isUpdateUserLoading){}
if(isUpdateUserError){}
if(isUpdateUserSuccessful){
//dosomething with data
return <SOME_HTML_OR_COMPONENT data={updatedUser}>
}
}
// The hook return value. `react-query`, `apollo-client`, `SWR` follow this return style object (same names)
// https://react-query-beta.tanstack.com/comparison
/**/
* Then `return` object interface for our custom hooks that do any type of data fetching
* whether it be fetching or mutating data.
*/
export interface IQueryState {
error: null;
isError: boolean;
isIdle: boolean;
isLoading: boolean;
isSuccess: boolean;
data?: any;
// allow other properties to be added (Ex: expose methods that update state above `addUser`, `deleteBook`)
[x: string]: any;
}
@cliffordfajardo

This comment has been minimized.

Copy link
Owner Author

@cliffordfajardo cliffordfajardo commented Oct 13, 2020

Hey __,
After speaking with you & ___, I agree that the right approach would be to try out this new pattern starting with just 1 component to allow us to incrementally evaluate adoption, while still adding value to the business

Other thoughts about react-query:
I was hesitant at first to introduce react-query in the codebase, before converting our current business logic to custom hooks, but I realized a few things:

  • react-query is only 5kb in size 🎉
  • any leftover business logic (filtering/transforming data) inside our components can easily be moved to their respective services files (bookAPI, userAPI, googleBookAPI, authorAPI) and it looks like we're already doing that in many places.
  • using react-query would:
    • eliminate the need to write the boiler hook code I showed in the gist file above 001-vanilla-react-hook.jsx
    • eliminate the need to write tests for the boiler plate code above (001-vanilla-react-hook.jsx)because the boilerplate code is already test and abstracted away in the react-query library 🎉
    • allow us to just consume the hooks:
    import { useQuery } from 'react-query'
    import BOOK_API from '....'
    
    function ExampleComponent() {
        const { isLoading, error, data: book } = useQuery(bookId, BOOK_API.get(bookId))
    )
    • eliminate the need write code that de-duplicates requests that often fire when react re-renders components.
    • eliminate the need to write code/think about caching or stale data management since react-query config and options comes for free inside the react-query library
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment