Skip to content

Latest commit

 

History

History
146 lines (121 loc) · 4.43 KB

README.md

File metadata and controls

146 lines (121 loc) · 4.43 KB

Remote Resource Toolkit

Simple React hooks to avoid redux boilerplate and fetching remote data. Fetched data is cached in redux.

Most of the React projects I worked on use redux as a state management solution. Even with the recommended Redux toolkit I noticed repetitive codes while writing redux logic for different portion of the application.

I thought of developing a common react hook which sums up most of the actions that performed on a state slice. So that I can avoid writing slice for different remote data in project

The idea is to have a common redux slice, say, remoteResource and other slices will be added/removed dynamically under it.

What is Remote resource?

This hook view a remote resource as a combination of two things, resource data and parameters(query params + request body). So all remote resource under redux structured as below:

interface RemoteResource<ResourceType,ResourceParams> {
  status: ApiCallStatus;
  resource?: ResourceType;
  resourceKey?: string;
  resourceParams?: ResourceParams;
  message?: string;
}

The above structure get added/removed under state.remoteResource when useRemoteResource called.

How to use

Add the common slice to the store

import {remoteResourceReducer} from '@cksutils/remote-resource-toolkit';

export const store = configureStore({
  reducer: {
    remoteResource: remoteResourceReducer,
  },
});

That's it no need to create any slice in the application, instead use useRemoteResource to load data.

Create your own hooks using useRemoteResource for loading a specific type of resource.

Say, we want to load products from api /api/products. sortByColumn product name and perPage 20 items

import {ResourceLoaderFn} from '@cksutils/remote-resource-toolkit';

// ResourceType, which is taken as items and totalCount
type ProductList = { items: ProductModel[]; totalCount: number };

// This Resource params
const deafultQueryParams: ResourceFilters = {
  page: 1,
  perPage: 20,
  sortByColumn: 'name',
  sortOrder: 'asc',
};

/**
 * We have to provide a resource loader function.
 * It takes below argument
 * args: {
 *   params: Partial<Params>;
 *   abortSignal?: AbortSignal;
 *  }
 * abortsignal to abort the network call if component unmounts before response
 * and return type is 
 * Promise<{ success: boolean; data?: ResourceType; params?: Params; message?: string }>;
 **/
const resourceLoader: ResourceLoaderFn<ProductList, ResourceFilters> = async (args) => {
  try {
    const { data } = await axios.get('/api/products', {
      params: {
        ...deafultQueryParams,
        ...args.params,
      },
      signal: args?.abortSignal,
    });
    return {
      success: data.success,
      data: {
        items: data?.data?.products,
        totalCount: data?.data?.total_count,
      },
      message: data?.message,
    };
  } catch (e) {
    return { success: false };
  }
};

const useProductList = (params: { autoLoad?: boolean; autoRemove?: boolean } = EMPTY_OBJECT) => {
  const {
    status,
    resource,
    resourceParams = deafultQueryParams,
    loadData,
  } = useRemoteResource<ProductList, ResourceFilters>({
    resourceName: 'products',// this string will be used to add in redux, so data will be available at state.remoteResource.products
    autoLoad: params.autoLoad, // whether to load data on mount
    autoRemove: params.autoRemove, // whether to remove from redux on unmount
    resourceLoader, // the resource loader function
  });

  // Compose some custom methods from the useRemoteResource
  const moveToPage = async (page: number) => {
    return loadData({ page });
  };

  const searchProduct = (searchText: string) => loadData({ searchText });

  return {
    isLoading: status === 'IN_PROGRESS',
    status,
    items: resource?.items || EMPTY_ARRAY,
    totalCount: resource?.totalCount || 0,
    filters: resourceParams,
    loadProducts: loadData,
    refreshData: loadData,
    moveToPage,
    searchProduct,
  };
};

export default useProductList;

Use the customHook to load product data anywhere

// ProductList.tsx
import useProductList from './useProductList';

export function productList(){
    const {isLoading, items,filters, moveToPage } = useProductList({autoLoad: true});

    return(
        <table>
         <tr><th>Name</th><th>Price</th></tr>
         {item.map(product=>(<tr>
          <td>{product.name}</td>
          <td>product.price</td>
         </tr>)}
        </table>
    )
}