Skip to content

Instantly share code, notes, and snippets.

@cliffordfajardo
Last active Mar 17, 2022
Embed
What would you like to do?
Managing data and business logic in React with custom hooks + React Query

Brief Why React Hooks?

React hooks provide an excellent way to abstract away business logic out of your component;

  • ideally if your using hooks well, your React components should be very lighweight and mostly you will only see hooks being used at the top of your React function component and your markup returned at the end.

hooks can only be used with React function components, not class components.

  • Prior to react hooks, there was not an easy way to create reusable abstractions as easily since React class components forced you to split your logic into different methods

hooks make testing and managing abstractions easier (IMO) Consider watching these videos:

TLDR:

They 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

Concerning React Query

React Query was created by Tanner Linsley in 2020; he is also the author of multiple react libraries with 10k+ stars on github.

If you're using React and you don't know the difference between "client vs server state" in modern web application, consider watching 🍿 React Query: It’s Time to Break up with your "Global State”! – Tanner Linsley. This is a relatively new concept which only has gained popularity in the past 2 years (mid 2020+). This video might be especially of interest to folks who are using Redux (or similar client state libraries) for storing API data. You can use client state libraries for storing your API data, but its now seen as an anti-pattern & you're likely missing out on a lot of free things when you are not using a server state library in your UI and you may be writing more complicated code than perhaps is required.

Nowdays the majority of modern react apps could get by without using any client state libraries. Personal Example

  • I used React on the web and React Native for over 1 year outside of LinkedIn and we did not have any client state library in our app. We used React context using the "Context hooks pattern" (very common in good codebases, not enough folks talk about it). At LinkedIn I work on 2 multiproducts that don't use any client state libraries. Infradev team is moving away from Redux too.
  • I know friends at many company's Uber, Netflix etc that allow maintain many apps without any external 3rd party library for client state.

A recommendation I give to folks is use React's builtin context to begin with, try to stretch its usage until you need something a lot more advanced (recoil, zustand, etc). You will get far with React's context, and when you hit challenges read into React.useMemo, React.useCallback, useRef APIs and other APIs to help you.

Some strong use cases for client state librarie's are web apps with VERY complex UI interaction's and tens of thousands of elements potentially. Recoil: State Management for Today's React by Dave McCabe from React core team highlights such uses cases.

Custom Hooks in React: The Ultimate UI Abstraction Layer - Tanner Linsley

  1. 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 useCreateBookPost(): 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(){
const bookDataInfo = useCreateBookPost();
// 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(bookDataInfo.isError){ return <SOME_ERROR_BANNER>}
if(bookDataInfo.data){
return <SOME_HTML_OR_COMPONENT data={bookDataInfo.data}>
}
if(userDataInfo.isLoading){ <PageLoadingSpinner /> }
if(userDataInfo.data){
return <SOME_HTML_OR_COMPONENT data={userDataInfo.data}>
}
return (
<button onClick={() => {
bookDataInfo.postBook({ ...some_new_data})
}}>
)
}
// 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
Copy link
Author

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