Passed
Push — feature/data-request-hooks ( 13824f )
by Tristan
06:12
created

indexResourceHook.ts ➔ useResourceIndex   F

Complexity

Conditions 23

Size

Total Lines 96
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 84
dl 0
loc 96
c 0
b 0
f 0
rs 0
cc 23

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like indexResourceHook.ts ➔ useResourceIndex often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
2
import {
3
  FetchError,
4
  postRequest,
5
  processJsonResponse,
6
} from "../../helpers/httpRequests";
7
import { identity } from "../../helpers/queries";
8
import indexCrudReducer, {
9
  initializeState,
10
  ResourceState,
11
  ActionTypes,
12
} from "./indexCrudReducer";
13
import { Json, ResourceStatus } from "./types";
14
15
type IndexedObject<T> = {
16
  [id: number]: T;
17
};
18
19
function valuesSelector<T extends { id: number }>(
20
  state: ResourceState<T>,
21
): IndexedObject<T> {
22
  return Object.values(state.values).reduce(
23
    (collection: IndexedObject<T>, item) => {
24
      collection[item.value.id] = item.value;
25
      return collection;
26
    },
27
    {},
28
  );
29
}
30
function statusSelector<T extends { id: number }>(
31
  state: ResourceState<T>,
32
): IndexedObject<ResourceStatus> {
33
  // If the entire index is being refreshed, then each individual item should be considered "pending".
34
  const forcePending = state.indexMeta.status === "pending";
35
  return Object.values(state.values).reduce(
36
    (collection: IndexedObject<ResourceStatus>, item) => {
37
      collection[item.value.id] = forcePending ? "pending" : item.status;
38
      return collection;
39
    },
40
    {},
41
  );
42
}
43
44
export function useResourceIndex<T extends { id: number }>(
45
  endpoint: string,
46
  overrides?: {
47
    initialValue?: T[]; // Defaults to an empty list.
48
    forceInitialRefresh: boolean; // If you set an initialValue but also want to refresh immediately, set this to true.
49
    parseIndexResponse?: (response: Json) => T[];
50
    parseEntityResponse?: (response: Json) => T;
51
    resolveEntityEndpoint?: (baseEndpoint: string, entity: T) => string;
52
    resolveCreateEndpoint?: (baseEndpoint: string, newEntity: T) => string;
53
    handleError?: (error: Error | FetchError) => void;
54
  },
55
): {
56
  values: IndexedObject<T>;
57
  indexStatus: ResourceStatus;
58
  entityStatus: IndexedObject<ResourceStatus>;
59
  create: (newValue: T) => Promise<T>;
60
  refresh: () => Promise<T[]>; // Reloads the entire index.
61
  update: (newValue: T) => Promise<T>;
62
  deleteResource: (id: number) => Promise<void>;
63
} {
64
  const initialValue = overrides?.initialValue ?? [];
65
  const forceInitialRefresh =
66
    overrides?.initialValue !== undefined && overrides?.forceInitialRefresh;
67
  const parseIndexResponse = overrides?.parseIndexResponse ?? identity;
68
  const parseEntityResponse = overrides?.parseEntityResponse ?? identity;
69
  const resolveEntityEndpoint =
70
    overrides?.resolveEntityEndpoint ??
71
    ((baseEndpoint, entity): string => `${baseEndpoint}/${entity.id}`);
72
  const resolveCreateEndpoint =
73
    overrides?.resolveCreateEndpoint ??
74
    ((baseEndpoint, _): string => baseEndpoint);
75
  const handleError =
76
    overrides?.handleError ??
77
    ((): void => {
78
      /* Do nothing. */
79
    });
80
81
  const isSubscribed = useRef(true);
82
83
  const [state, dispatch] = useReducer(
84
    indexCrudReducer,
85
    initialValue,
86
    initializeState,
87
  );
88
89
  const values = useMemo(() => valuesSelector(state), [state]);
90
  const indexStatus = state.indexMeta.status;
91
  const entityStatus = useMemo(() => statusSelector(state), [state]);
92
93
  const create = useCallback(
94
    async (newValue: T): Promise<T> => {
95
      dispatch({
96
        type: ActionTypes.createStart,
97
        meta: { item: newValue },
98
      });
99
      let json: Json;
100
      try {
101
        json = await postRequest(
102
          resolveCreateEndpoint(endpoint, newValue),
103
          newValue,
104
        ).then(processJsonResponse);
105
      } catch (error) {
106
        dispatch({
107
          type: ActionTypes.createReject,
108
          payload: error,
109
          meta: { item: newValue },
110
        });
111
      }
112
      const entity = parseEntityResponse(json) as T;
113
      if (isSubscribed.current) {
114
        dispatch({
115
          type: ActionTypes.createFulfill,
116
          payload: entity,
117
          meta: { item: newValue },
118
        });
119
      }
120
      return entity;
121
    },
122
    [endpoint, resolveCreateEndpoint, parseEntityResponse],
123
  );
124
125
  // Unsubscribe from promises when this hook is unmounted.
126
  useEffect(() => {
127
    return (): void => {
128
      isSubscribed.current = false;
129
    };
130
  }, []);
131
132
  return {
133
    values,
134
    indexStatus,
135
    entityStatus,
136
    create,
137
    update,
138
    refresh,
139
  };
140
}
141
142
export default useResourceIndex;
143