@ahoo-wang/fetcher-react 2.9.0 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +499 -27
  2. package/package.json +1 -3
package/README.md CHANGED
@@ -26,18 +26,18 @@ robust data fetching capabilities.
26
26
 
27
27
  - [Installation](#installation)
28
28
  - [Usage](#usage)
29
- - [useFetcher Hook](#usefetcher-hook)
30
- - [useExecutePromise Hook](#useexecutepromise-hook)
31
- - [usePromiseState Hook](#usepromisestate-hook)
32
- - [useRequestId Hook](#userequestid-hook)
33
- - [useLatest Hook](#uselatest-hook)
34
- - [useKeyStorage Hook](#usekeystorage-hook)
35
- - [Wow Query Hooks](#wow-query-hooks)
36
- - [useListQuery Hook](#uselistquery-hook)
37
- - [usePagedQuery Hook](#usepagedquery-hook)
38
- - [useSingleQuery Hook](#usesinglequery-hook)
39
- - [useCountQuery Hook](#usecountquery-hook)
40
- - [useListStreamQuery Hook](#useliststreamquery-hook)
29
+ - [useFetcher Hook](#usefetcher-hook)
30
+ - [useExecutePromise Hook](#useexecutepromise-hook)
31
+ - [usePromiseState Hook](#usepromisestate-hook)
32
+ - [useRequestId Hook](#userequestid-hook)
33
+ - [useLatest Hook](#uselatest-hook)
34
+ - [useKeyStorage Hook](#usekeystorage-hook)
35
+ - [Wow Query Hooks](#wow-query-hooks)
36
+ - [useListQuery Hook](#uselistquery-hook)
37
+ - [usePagedQuery Hook](#usepagedquery-hook)
38
+ - [useSingleQuery Hook](#usesinglequery-hook)
39
+ - [useCountQuery Hook](#usecountquery-hook)
40
+ - [useListStreamQuery Hook](#useliststreamquery-hook)
41
41
  - [Best Practices](#best-practices)
42
42
  - [API Reference](#api-reference)
43
43
  - [License](#license)
@@ -724,6 +724,478 @@ const MyComponent = () => {
724
724
  - Use `useKeyStorage` for persistent client-side data
725
725
  - Implement optimistic updates for better UX
726
726
 
727
+ ## 🚀 Advanced Usage Examples
728
+
729
+ ### Custom Hook Composition
730
+
731
+ Create reusable hooks by composing multiple fetcher-react hooks:
732
+
733
+ ```typescript jsx
734
+ import { useFetcher, usePromiseState, useLatest } from '@ahoo-wang/fetcher-react';
735
+ import { useCallback, useEffect } from 'react';
736
+
737
+ function useUserProfile(userId: string) {
738
+ const latestUserId = useLatest(userId);
739
+ const { loading, result: profile, error, execute } = useFetcher();
740
+
741
+ const fetchProfile = useCallback(() => {
742
+ execute({
743
+ url: `/api/users/${latestUserId.current}`,
744
+ method: 'GET'
745
+ });
746
+ }, [execute, latestUserId]);
747
+
748
+ useEffect(() => {
749
+ if (userId) {
750
+ fetchProfile();
751
+ }
752
+ }, [userId, fetchProfile]);
753
+
754
+ return { profile, loading, error, refetch: fetchProfile };
755
+ }
756
+
757
+ // Usage
758
+ function UserProfile({ userId }: { userId: string }) {
759
+ const { profile, loading, error, refetch } = useUserProfile(userId);
760
+
761
+ if (loading) return <div>Loading...</div>;
762
+ if (error) return <div>Error: {error.message}</div>;
763
+
764
+ return (
765
+ <div>
766
+ <h2>{profile?.name}</h2>
767
+ <button onClick={refetch}>Refresh</button>
768
+ </div>
769
+ );
770
+ }
771
+ ```
772
+
773
+ ### Error Boundaries Integration
774
+
775
+ Integrate with React Error Boundaries for better error handling:
776
+
777
+ ```typescript jsx
778
+ import { Component, ErrorInfo, ReactNode } from 'react';
779
+
780
+ class FetchErrorBoundary extends Component<
781
+ { children: ReactNode; fallback?: ReactNode },
782
+ { hasError: boolean; error?: Error }
783
+ > {
784
+ constructor(props: { children: ReactNode; fallback?: ReactNode }) {
785
+ super(props);
786
+ this.state = { hasError: false };
787
+ }
788
+
789
+ static getDerivedStateFromError(error: Error) {
790
+ return { hasError: true, error };
791
+ }
792
+
793
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
794
+ console.error('Fetch error boundary caught an error:', error, errorInfo);
795
+ }
796
+
797
+ render() {
798
+ if (this.state.hasError) {
799
+ return this.props.fallback || <div>Something went wrong.</div>;
800
+ }
801
+
802
+ return this.props.children;
803
+ }
804
+ }
805
+
806
+ // Usage with hooks
807
+ function DataComponent() {
808
+ const { result, loading, error, execute } = useFetcher();
809
+
810
+ // Error will be caught by boundary if thrown
811
+ if (error) {
812
+ throw error;
813
+ }
814
+
815
+ return (
816
+ <div>
817
+ {loading ? 'Loading...' : JSON.stringify(result)}
818
+ </div>
819
+ );
820
+ }
821
+
822
+ // Wrap components that use fetcher hooks
823
+ function App() {
824
+ return (
825
+ <FetchErrorBoundary fallback={<div>Failed to load data</div>}>
826
+ <DataComponent />
827
+ </FetchErrorBoundary>
828
+ );
829
+ }
830
+ ```
831
+
832
+ ### Suspense Integration
833
+
834
+ Use with React Suspense for better loading states:
835
+
836
+ ```typescript jsx
837
+ import { Suspense, useState } from 'react';
838
+ import { useFetcher } from '@ahoo-wang/fetcher-react';
839
+
840
+ // Create a resource that throws a promise
841
+ function createDataResource<T>(promise: Promise<T>) {
842
+ let status = 'pending';
843
+ let result: T;
844
+ let error: Error;
845
+
846
+ const suspender = promise.then(
847
+ (data) => {
848
+ status = 'success';
849
+ result = data;
850
+ },
851
+ (err) => {
852
+ status = 'error';
853
+ error = err;
854
+ }
855
+ );
856
+
857
+ return {
858
+ read() {
859
+ if (status === 'pending') {
860
+ throw suspender;
861
+ } else if (status === 'error') {
862
+ throw error;
863
+ } else {
864
+ return result;
865
+ }
866
+ }
867
+ };
868
+ }
869
+
870
+ function DataComponent({ resource }: { resource: any }) {
871
+ const data = resource.read(); // This will throw if pending
872
+ return <div>{JSON.stringify(data)}</div>;
873
+ }
874
+
875
+ function App() {
876
+ const [resource, setResource] = useState<any>(null);
877
+
878
+ const handleFetch = () => {
879
+ const { execute } = useFetcher();
880
+ const promise = execute({ url: '/api/data', method: 'GET' });
881
+ setResource(createDataResource(promise));
882
+ };
883
+
884
+ return (
885
+ <div>
886
+ <button onClick={handleFetch}>Fetch Data</button>
887
+ <Suspense fallback={<div>Loading...</div>}>
888
+ {resource && <DataComponent resource={resource} />}
889
+ </Suspense>
890
+ </div>
891
+ );
892
+ }
893
+ ```
894
+
895
+ ### Performance Optimization Patterns
896
+
897
+ Advanced patterns for optimal performance:
898
+
899
+ ```typescript jsx
900
+ import { useMemo, useCallback, useRef } from 'react';
901
+ import { useListQuery } from '@ahoo-wang/fetcher-react';
902
+
903
+ function OptimizedDataTable({ filters, sortBy }) {
904
+ // Memoize query configuration to prevent unnecessary re-executions
905
+ const queryConfig = useMemo(() => ({
906
+ condition: filters,
907
+ sort: [{ field: sortBy, order: 'asc' }],
908
+ limit: 50
909
+ }), [filters, sortBy]);
910
+
911
+ const { result, loading, execute, setCondition } = useListQuery({
912
+ initialQuery: queryConfig,
913
+ execute: useCallback(async (query) => {
914
+ // Debounce API calls
915
+ await new Promise(resolve => setTimeout(resolve, 300));
916
+ return fetchData(query);
917
+ }, []),
918
+ autoExecute: true
919
+ });
920
+
921
+ // Use ref to track latest filters without causing re-renders
922
+ const filtersRef = useRef(filters);
923
+
924
+ useEffect(() => {
925
+ filtersRef.current = filters;
926
+ });
927
+
928
+ // Debounced search
929
+ const debouncedSearch = useMemo(
930
+ () => debounce((searchTerm: string) => {
931
+ setCondition({ ...filtersRef.current, search: searchTerm });
932
+ }, 500),
933
+ [setCondition]
934
+ );
935
+
936
+ return (
937
+ <div>
938
+ <input
939
+ onChange={(e) => debouncedSearch(e.target.value)}
940
+ placeholder="Search..."
941
+ />
942
+ {loading ? 'Loading...' : (
943
+ <table>
944
+ <tbody>
945
+ {result?.map(item => (
946
+ <tr key={item.id}>
947
+ <td>{item.name}</td>
948
+ </tr>
949
+ ))}
950
+ </tbody>
951
+ </table>
952
+ )}
953
+ </div>
954
+ );
955
+ }
956
+
957
+ // Debounce utility
958
+ function debounce<T extends (...args: any[]) => any>(
959
+ func: T,
960
+ wait: number
961
+ ): (...args: Parameters<T>) => void {
962
+ let timeout: NodeJS.Timeout;
963
+ return (...args: Parameters<T>) => {
964
+ clearTimeout(timeout);
965
+ timeout = setTimeout(() => func(...args), wait);
966
+ };
967
+ }
968
+ ```
969
+
970
+ ### Real-World Integration Examples
971
+
972
+ Complete examples showing integration with popular libraries:
973
+
974
+ #### With React Query (TanStack Query)
975
+
976
+ ```typescript jsx
977
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
978
+ import { useFetcher } from '@ahoo-wang/fetcher-react';
979
+
980
+ function useUserData(userId: string) {
981
+ return useQuery({
982
+ queryKey: ['user', userId],
983
+ queryFn: async () => {
984
+ const { execute } = useFetcher();
985
+ const result = await execute({
986
+ url: `/api/users/${userId}`,
987
+ method: 'GET'
988
+ });
989
+ return result;
990
+ }
991
+ });
992
+ }
993
+
994
+ function UserProfile({ userId }: { userId: string }) {
995
+ const { data, isLoading, error } = useUserData(userId);
996
+
997
+ if (isLoading) return <div>Loading...</div>;
998
+ if (error) return <div>Error: {error.message}</div>;
999
+
1000
+ return <div>Welcome, {data.name}!</div>;
1001
+ }
1002
+ ```
1003
+
1004
+ #### With Redux Toolkit
1005
+
1006
+ ```typescript jsx
1007
+ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
1008
+ import { useFetcher } from '@ahoo-wang/fetcher-react';
1009
+
1010
+ const fetchUserData = createAsyncThunk(
1011
+ 'user/fetchData',
1012
+ async (userId: string) => {
1013
+ const { execute } = useFetcher();
1014
+ return await execute({
1015
+ url: `/api/users/${userId}`,
1016
+ method: 'GET'
1017
+ });
1018
+ }
1019
+ );
1020
+
1021
+ const userSlice = createSlice({
1022
+ name: 'user',
1023
+ initialState: { data: null, loading: false, error: null },
1024
+ reducers: {},
1025
+ extraReducers: (builder) => {
1026
+ builder
1027
+ .addCase(fetchUserData.pending, (state) => {
1028
+ state.loading = true;
1029
+ })
1030
+ .addCase(fetchUserData.fulfilled, (state, action) => {
1031
+ state.loading = false;
1032
+ state.data = action.payload;
1033
+ })
1034
+ .addCase(fetchUserData.rejected, (state, action) => {
1035
+ state.loading = false;
1036
+ state.error = action.error.message;
1037
+ });
1038
+ }
1039
+ });
1040
+
1041
+ function UserComponent({ userId }: { userId: string }) {
1042
+ const dispatch = useDispatch();
1043
+ const { data, loading, error } = useSelector((state) => state.user);
1044
+
1045
+ useEffect(() => {
1046
+ dispatch(fetchUserData(userId));
1047
+ }, [userId, dispatch]);
1048
+
1049
+ if (loading) return <div>Loading...</div>;
1050
+ if (error) return <div>Error: {error}</div>;
1051
+
1052
+ return <div>{data?.name}</div>;
1053
+ }
1054
+ ```
1055
+
1056
+ #### With Zustand
1057
+
1058
+ ```typescript jsx
1059
+ import { create } from 'zustand';
1060
+ import { useFetcher } from '@ahoo-wang/fetcher-react';
1061
+
1062
+ interface UserStore {
1063
+ user: any;
1064
+ loading: boolean;
1065
+ error: string | null;
1066
+ fetchUser: (userId: string) => Promise<void>;
1067
+ }
1068
+
1069
+ const useUserStore = create<UserStore>((set) => ({
1070
+ user: null,
1071
+ loading: false,
1072
+ error: null,
1073
+ fetchUser: async (userId) => {
1074
+ set({ loading: true, error: null });
1075
+ try {
1076
+ const { execute } = useFetcher();
1077
+ const user = await execute({
1078
+ url: `/api/users/${userId}`,
1079
+ method: 'GET'
1080
+ });
1081
+ set({ user, loading: false });
1082
+ } catch (error) {
1083
+ set({ error: error.message, loading: false });
1084
+ }
1085
+ }
1086
+ }));
1087
+
1088
+ function UserComponent({ userId }: { userId: string }) {
1089
+ const { user, loading, error, fetchUser } = useUserStore();
1090
+
1091
+ useEffect(() => {
1092
+ fetchUser(userId);
1093
+ }, [userId, fetchUser]);
1094
+
1095
+ if (loading) return <div>Loading...</div>;
1096
+ if (error) return <div>Error: {error}</div>;
1097
+
1098
+ return <div>{user?.name}</div>;
1099
+ }
1100
+ ```
1101
+
1102
+ ### Testing Patterns
1103
+
1104
+ Comprehensive testing examples for hooks:
1105
+
1106
+ ```typescript jsx
1107
+ import { renderHook, act, waitFor } from '@testing-library/react';
1108
+ import { useFetcher, useListQuery } from '@ahoo-wang/fetcher-react';
1109
+
1110
+ // Mock fetcher
1111
+ jest.mock('@ahoo-wang/fetcher', () => ({
1112
+ Fetcher: jest.fn().mockImplementation(() => ({
1113
+ request: jest.fn(),
1114
+ })),
1115
+ }));
1116
+
1117
+ describe('useFetcher', () => {
1118
+ it('should handle successful fetch', async () => {
1119
+ const mockData = { id: 1, name: 'Test' };
1120
+ const mockFetcher = { request: jest.fn().mockResolvedValue(mockData) };
1121
+
1122
+ const { result } = renderHook(() => useFetcher({ fetcher: mockFetcher }));
1123
+
1124
+ act(() => {
1125
+ result.current.execute({ url: '/api/test', method: 'GET' });
1126
+ });
1127
+
1128
+ await waitFor(() => {
1129
+ expect(result.current.loading).toBe(false);
1130
+ expect(result.current.result).toEqual(mockData);
1131
+ expect(result.current.error).toBe(null);
1132
+ });
1133
+ });
1134
+
1135
+ it('should handle fetch error', async () => {
1136
+ const mockError = new Error('Network error');
1137
+ const mockFetcher = { request: jest.fn().mockRejectedValue(mockError) };
1138
+
1139
+ const { result } = renderHook(() => useFetcher({ fetcher: mockFetcher }));
1140
+
1141
+ act(() => {
1142
+ result.current.execute({ url: '/api/test', method: 'GET' });
1143
+ });
1144
+
1145
+ await waitFor(() => {
1146
+ expect(result.current.loading).toBe(false);
1147
+ expect(result.current.error).toEqual(mockError);
1148
+ expect(result.current.result).toBe(null);
1149
+ });
1150
+ });
1151
+ });
1152
+
1153
+ describe('useListQuery', () => {
1154
+ it('should manage query state', async () => {
1155
+ const mockData = [{ id: 1, name: 'Item 1' }];
1156
+ const mockExecute = jest.fn().mockResolvedValue(mockData);
1157
+
1158
+ const { result } = renderHook(() =>
1159
+ useListQuery({
1160
+ initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
1161
+ execute: mockExecute,
1162
+ }),
1163
+ );
1164
+
1165
+ act(() => {
1166
+ result.current.execute();
1167
+ });
1168
+
1169
+ await waitFor(() => {
1170
+ expect(result.current.loading).toBe(false);
1171
+ expect(result.current.result).toEqual(mockData);
1172
+ });
1173
+
1174
+ expect(mockExecute).toHaveBeenCalledWith({
1175
+ condition: {},
1176
+ projection: {},
1177
+ sort: [],
1178
+ limit: 10,
1179
+ });
1180
+ });
1181
+
1182
+ it('should update condition', () => {
1183
+ const { result } = renderHook(() =>
1184
+ useListQuery({
1185
+ initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
1186
+ execute: jest.fn(),
1187
+ }),
1188
+ );
1189
+
1190
+ act(() => {
1191
+ result.current.setCondition({ status: 'active' });
1192
+ });
1193
+
1194
+ expect(result.current.condition).toEqual({ status: 'active' });
1195
+ });
1196
+ });
1197
+ ```
1198
+
727
1199
  ## API Reference
728
1200
 
729
1201
  ### useFetcher
@@ -745,10 +1217,10 @@ flexible configuration.
745
1217
  **Parameters:**
746
1218
 
747
1219
  - `options`: Configuration options or supplier function
748
- - `fetcher`: Custom fetcher instance to use. Defaults to the default fetcher.
749
- - `initialStatus`: Initial status, defaults to IDLE
750
- - `onSuccess`: Callback invoked on success
751
- - `onError`: Callback invoked on error
1220
+ - `fetcher`: Custom fetcher instance to use. Defaults to the default fetcher.
1221
+ - `initialStatus`: Initial status, defaults to IDLE
1222
+ - `onSuccess`: Callback invoked on success
1223
+ - `onError`: Callback invoked on error
752
1224
 
753
1225
  **Returns:**
754
1226
 
@@ -780,9 +1252,9 @@ state options.
780
1252
  **Parameters:**
781
1253
 
782
1254
  - `options`: Configuration options
783
- - `initialStatus`: Initial status, defaults to IDLE
784
- - `onSuccess`: Callback invoked on success
785
- - `onError`: Callback invoked on error
1255
+ - `initialStatus`: Initial status, defaults to IDLE
1256
+ - `onSuccess`: Callback invoked on success
1257
+ - `onError`: Callback invoked on error
786
1258
 
787
1259
  **Returns:**
788
1260
 
@@ -814,9 +1286,9 @@ suppliers.
814
1286
  **Parameters:**
815
1287
 
816
1288
  - `options`: Configuration options or supplier function
817
- - `initialStatus`: Initial status, defaults to IDLE
818
- - `onSuccess`: Callback invoked on success (can be async)
819
- - `onError`: Callback invoked on error (can be async)
1289
+ - `initialStatus`: Initial status, defaults to IDLE
1290
+ - `onSuccess`: Callback invoked on success (can be async)
1291
+ - `onError`: Callback invoked on error (can be async)
820
1292
 
821
1293
  **Returns:**
822
1294
 
@@ -907,7 +1379,7 @@ A React hook for managing list queries with state management for conditions, pro
907
1379
  **Parameters:**
908
1380
 
909
1381
  - `options`: Configuration options including initialQuery and list function
910
- - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1382
+ - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
911
1383
 
912
1384
  **Returns:**
913
1385
 
@@ -932,7 +1404,7 @@ A React hook for managing paged queries with state management for conditions, pr
932
1404
  **Parameters:**
933
1405
 
934
1406
  - `options`: Configuration options including initialQuery and query function
935
- - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1407
+ - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
936
1408
 
937
1409
  **Returns:**
938
1410
 
@@ -957,7 +1429,7 @@ A React hook for managing single queries with state management for conditions, p
957
1429
  **Parameters:**
958
1430
 
959
1431
  - `options`: Configuration options including initialQuery and query function
960
- - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1432
+ - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
961
1433
 
962
1434
  **Returns:**
963
1435
 
@@ -981,7 +1453,7 @@ A React hook for managing count queries with state management for conditions.
981
1453
  **Parameters:**
982
1454
 
983
1455
  - `options`: Configuration options including initialQuery and execute function
984
- - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1456
+ - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
985
1457
 
986
1458
  **Returns:**
987
1459
 
@@ -1011,7 +1483,7 @@ Returns a readable stream of JSON server-sent events.
1011
1483
  **Parameters:**
1012
1484
 
1013
1485
  - `options`: Configuration options including initialQuery and listStream function
1014
- - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1486
+ - `autoExecute`: Whether to automatically execute the query on component mount (defaults to false)
1015
1487
 
1016
1488
  **Returns:**
1017
1489
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahoo-wang/fetcher-react",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "React integration for Fetcher HTTP client. Provides React Hooks and components for seamless data fetching with automatic re-rendering and loading states.",
5
5
  "keywords": [
6
6
  "fetch",
@@ -66,8 +66,6 @@
66
66
  "@types/react": "^18.3.26",
67
67
  "@types/react-dom": "^18.3.7",
68
68
  "jsdom": "^27.0.0",
69
- "react": "^18.3.1",
70
- "react-dom": "^18.3.1",
71
69
  "@vitejs/plugin-react": "^5.0.4"
72
70
  },
73
71
  "scripts": {