Passed
Push — feature/data-request-hooks ( b81437...8646f4 )
by Tristan
06:15
created

resources/assets/js/hooks/webResourceHooks/singleResourceHook.ts   A

Complexity

Total Complexity 13
Complexity/F 4.33

Size

Lines of Code 149
Function Count 3

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 118
mnd 10
bc 10
fnc 3
dl 0
loc 149
rs 10
bpm 3.3333
cpm 4.3333
noi 0
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A singleResourceHook.ts ➔ reducer 0 29 4
A singleResourceHook.ts ➔ decrement 0 8 2
B singleResourceHook.ts ➔ useResource 0 50 7
1
import { useEffect, useRef, useState } from "react";
2
import { useFetch } from "react-async";
3
import { fetchParameters, FetchError } from "../../helpers/httpRequests";
4
import { identity } from "../../helpers/queries";
5
import reducer from "./indexCrudReducer";
6
import { Json, ResourceStatus } from "./types";
7
8
export interface ResourceState<T> {
9
  value: T;
10
  status: ResourceStatus;
11
  pendingCount: number;
12
  error: Error | FetchError | undefined;
13
}
14
15
export enum ActionTypes {
16
  GetStart = "GET_START",
17
  GetFulfill = "GET_FULFILL",
18
  GetReject = "GET_REJECT",
19
20
  UpdateStart = "UPDATE_START",
21
  UpdateFulfill = "UPDATE_FULFILL",
22
  UpdateReject = "UPDATE_REJECT",
23
}
24
25
export type GetStartAction = { type: ActionTypes.GetStart };
26
export type GetFulfillAction<T> = {
27
  type: ActionTypes.GetFulfill;
28
  payload: T;
29
};
30
export type GetRejectAction = {
31
  type: ActionTypes.GetReject;
32
  payload: Error | FetchError;
33
};
34
35
export type UpdateStartAction<T> = {
36
  type: ActionTypes.UpdateStart;
37
  meta: { item: T };
38
};
39
export type UpdateFulfillAction<T> = {
40
  type: ActionTypes.UpdateFulfill;
41
  payload: T;
42
  meta: { item: T };
43
};
44
export type UpdateRejectAction<T> = {
45
  type: ActionTypes.UpdateReject;
46
  payload: Error | FetchError;
47
  meta: { item: T };
48
};
49
export type AsyncAction<T> =
50
  | GetStartAction
51
  | GetFulfillAction<T>
52
  | GetRejectAction
53
  | UpdateStartAction<T>
54
  | UpdateFulfillAction<T>
55
  | UpdateRejectAction<T>;
56
57
/**
58
 * Decrement the number if it above zero, else return 0.
59
 * This helps to avoid some pathological edge cases where pendingCount becomes permanently bugged.
60
 * @param num
61
 */
62
function decrement(num: number): number {
63
  return num <= 0 ? 0 : num - 1;
64
}
65
66
export function reducer<T>(
67
  state: ResourceState<T>,
68
  action: AsyncAction<T>,
69
): ResourceState<T> {
70
  switch (action.type) {
71
    case ActionTypes.GetStart:
72
      return {
73
        value: state.value,
74
        status: "pending",
75
        pendingCount: state.pendingCount + 1,
76
        error: undefined,
77
      };
78
    case ActionTypes.GetFulfill:
79
      return {
80
        value: action.payload,
81
        status: state.pendingCount === 1 ? "fulfilled" : "pending",
82
        pendingCount: decrement(state.pendingCount),
83
        error: undefined,
84
      };
85
    case ActionTypes.GetReject:
86
      return {
87
        value: state.value,
88
        status: state.pendingCount === 1 ? "rejected" : "pending",
89
        pendingCount: decrement(state.pendingCount),
90
        error: action.payload,
91
      };
92
    default:
93
      return state;
94
  }
95
}
96
97
export function useResource<T>(
98
  endpoint: string,
99
  initialValue: T,
100
  overrides?: {
101
    parseResponse?: (response: Json) => T; // Defaults to the identity function.
102
    skipInitialFetch?: boolean; // Defaults to false. Override if you want to keep the initialValue until refresh is called manually.
103
  },
104
): {
105
  value: T;
106
  status: ResourceStatus;
107
  error: undefined | Error | FetchError;
108
  update: (newValue: T) => void;
109
  refresh: () => void;
110
} {
111
  const internalParseResponse = overrides?.parseResponse ?? identity;
112
  const skipInitialFetch = overrides?.skipInitialFetch === true;
113
  const isSubscribed = useRef(true);
114
  const [value, setValue] = useState(initialValue);
115
  const { error, status, reload, run } = useFetch(
116
    endpoint,
117
    fetchParameters("GET"),
118
    {
119
      onResolve: (data) => {
120
        if (isSubscribed.current) {
121
          setValue(internalParseResponse(data));
122
        }
123
      },
124
      initialValue: null, // Setting this prevents fetch from happening on first render. (We call it later if necessary.)
125
    },
126
  );
127
  const refresh = reload;
128
  const update = (newValue: T): void => run(fetchParameters("PUT", newValue));
129
130
  // Despite the usual useEffect guidelines, this should only run on first render or when endpoint changes.
131
  // Changing skipInitialFetch after the first render should not cause a refresh.
132
  useEffect(() => {
133
    if (!skipInitialFetch) {
134
      refresh();
135
    }
136
  }, [endpoint]);
137
138
  // Unsubscribe from promises when this hook is unmounted.
139
  useEffect(() => {
140
    return (): void => {
141
      isSubscribed.current = false;
142
    };
143
  }, []);
144
145
  return { value, status, error, update, refresh };
146
}
147
148
export default useResource;
149