Skip to content

Commit ece2c2a

Browse files
committed
Fix: Refactor selectAPICallMultiple & reducer API_CALL & API_CALL_SUCCESS to handle multiple re-renders
1 parent 0270b4b commit ece2c2a

File tree

3 files changed

+174
-7
lines changed

3 files changed

+174
-7
lines changed

src/store/api/reducer.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,24 @@ const APIReducer = (state = INIT_STATE, action) => {
5151
case API_CALL:
5252
let api = getAPIFn(action.apiName);
5353
let key = api.urlFunction(...(action.args || []));
54-
let newCallState = state.calls[key] ? { ...state.calls[key] } : {};
55-
if (newCallState.state !== "LOADED") newCallState.state = "LOADING";
56-
if (action.retry !== undefined) newCallState.retries = action.retry;
57-
state = { ...state, calls: { ...state.calls, [key]: newCallState } };
54+
state = modifyNode(state, ["calls", key], (call) => {
55+
if (call !== undefined && call.state === "LOADED" && call.retries === undefined) {
56+
return call;
57+
}
58+
call = call || {};
59+
call.state = call.state !== "LOADED" ? "LOADING" : call.state;
60+
if (action.retry !== undefined) call.retries = action.retry;
61+
return call;
62+
});
5863
break;
5964

6065
case API_CALL_SUCCESS:
6166
state = modifyNode(state, ["call_metadata", action.call_key], (x) => {
6267
return { ...(x || {}), timestamp: action.timestamp };
6368
});
64-
state = modifyNode(state, ["calls", action.call_key], () => {
69+
state = modifyNode(state, ["calls", action.call_key], (call) => {
70+
if (call !== undefined && call.state === "LOADED" && call.retries === undefined && call.value === action.value)
71+
return call;
6572
return { state: "LOADED", value: action.value, code: action.code };
6673
});
6774
break;

src/store/api/selector.test.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import _ from "lodash";
2+
import Big from "big.js";
3+
import store from "../index.js";
4+
import assert from "assert";
5+
import * as apiRegistry from "../../helpers/apiRegistry";
6+
import { selectAPICallMultiple, getCallKey } from "./selectors";
7+
import MockAdapter from "axios-mock-adapter";
8+
import * as api_calls from "../../utils/helpers/api_calls";
9+
import * as mock_helper from "../../helpers/mock_helper";
10+
import axios from "axios";
11+
12+
const sinon = require("sinon");
13+
14+
const baseUrl = "https://testapi.com/api";
15+
const currencyAddress = "0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8";
16+
let axiosMock;
17+
18+
beforeEach(() => {
19+
axiosMock = new MockAdapter(axios);
20+
mock_helper.setupRiskFieldGets(axiosMock, currencyAddress);
21+
22+
apiRegistry.registerAPI(
23+
"apy",
24+
(address, days = 7) => `${baseUrl}/etokens/${address}/apr/?days_from=${days}`,
25+
(response) => api_calls.toDecimal(response.apy)
26+
);
27+
28+
apiRegistry.registerAPI(
29+
"activePremiums",
30+
(address, daysFrom = 90, daysTo) =>
31+
`${baseUrl}/riskmodules/${address}/active_premiums/` +
32+
api_calls.makeQueryParams(api_calls.addDaysParams(daysFrom, daysTo)),
33+
(response) => response.data
34+
);
35+
});
36+
37+
afterEach(() => {
38+
store.dispatch({ type: "RESET_ALL" });
39+
sinon.restore();
40+
axiosMock.restore();
41+
});
42+
43+
describe("selectAPICallMultiple with activePremiums and apy", () => {
44+
test("should return call_key for apy", () => {
45+
const expectedUrl = `${baseUrl}/etokens/${currencyAddress}/apr/?days_from=7`;
46+
const apiFunction = apiRegistry.getAPI("apy");
47+
48+
const callKey = apiFunction.urlFunction(currencyAddress);
49+
50+
expect(callKey).toEqual(expectedUrl);
51+
});
52+
53+
test("should return call_key for activePremiums", () => {
54+
const expectedUrl = `${baseUrl}/riskmodules/${currencyAddress}/active_premiums/?days_from=90`;
55+
const apiFunction = apiRegistry.getAPI("activePremiums");
56+
57+
const callKey = apiFunction.urlFunction(currencyAddress, 90);
58+
59+
expect(callKey).toEqual(expectedUrl);
60+
});
61+
62+
test("should return the corresponding calls for apy when present with actions", async () => {
63+
const call_key = getCallKey({ currentChain: { id: 1234, rpc: "https://foo-rpc.com/" } }, "apy", [currencyAddress]);
64+
store.dispatch({ type: "API_CALL", apiName: "apy", args: [currencyAddress] });
65+
66+
let result = selectAPICallMultiple(store.getState().APIReducer, [{ apiName: "apy", args: [currencyAddress] }]);
67+
expect(result).toEqual([{ state: "LOADING" }]);
68+
69+
store.dispatch({ type: "API_CALL_SUCCESS", call_key: call_key, value: 1000, timestamp: new Date().getTime() });
70+
result = selectAPICallMultiple(store.getState().APIReducer, [{ apiName: "apy", args: [currencyAddress] }]);
71+
expect(result).toEqual([{ state: "LOADED", value: 1000 }]);
72+
73+
let result2 = selectAPICallMultiple(store.getState().APIReducer, [{ apiName: "apy", args: [currencyAddress] }]);
74+
assert.strictEqual(result[0], result2[0]);
75+
});
76+
77+
test("should return the corresponding calls for activePremiums with different states", async () => {
78+
const call_key = getCallKey({ currentChain: { id: 1234, rpc: "https://foo-rpc.com/" } }, "activePremiums", [
79+
currencyAddress,
80+
]);
81+
store.dispatch({ type: "API_CALL", apiName: "activePremiums", args: [currencyAddress] });
82+
83+
let result = selectAPICallMultiple(store.getState().APIReducer, [
84+
{ apiName: "activePremiums", args: [currencyAddress] },
85+
]);
86+
expect(result).toEqual([{ state: "LOADING" }]);
87+
88+
store.dispatch({
89+
type: "API_CALL_SUCCESS",
90+
call_key: call_key,
91+
value: `ret${currencyAddress}activePremiums`,
92+
timestamp: new Date().getTime(),
93+
});
94+
95+
result = selectAPICallMultiple(store.getState().APIReducer, [
96+
{ apiName: "activePremiums", args: [currencyAddress] },
97+
]);
98+
99+
expect(result).toEqual([{ state: "LOADED", value: `ret${currencyAddress}activePremiums` }]);
100+
});
101+
102+
test("should handle multiple calls with different states for apy and activePremiums", async () => {
103+
const apyCallKey = getCallKey({ currentChain: { id: 1234, rpc: "https://foo-rpc.com/" } }, "apy", [
104+
currencyAddress,
105+
]);
106+
107+
const activePremiumsCallKey = getCallKey(
108+
{ currentChain: { id: 1234, rpc: "https://foo-rpc.com/" } },
109+
"activePremiums",
110+
[currencyAddress]
111+
);
112+
113+
store.dispatch({ type: "API_CALL", apiName: "apy", args: [currencyAddress] });
114+
store.dispatch({
115+
type: "API_CALL",
116+
apiName: "activePremiums",
117+
args: [currencyAddress],
118+
});
119+
120+
let result = selectAPICallMultiple(store.getState().APIReducer, [
121+
{ apiName: "apy", args: [currencyAddress] },
122+
{ apiName: "activePremiums", args: [currencyAddress] },
123+
]);
124+
125+
expect(result).toEqual([{ state: "LOADING" }, { state: "LOADING" }]);
126+
127+
store.dispatch({
128+
type: "API_CALL_SUCCESS",
129+
call_key: apyCallKey,
130+
value: 1000,
131+
timestamp: new Date().getTime(),
132+
});
133+
134+
result = selectAPICallMultiple(store.getState().APIReducer, [
135+
{ apiName: "apy", args: [currencyAddress] },
136+
{ apiName: "activePremiums", args: [currencyAddress] },
137+
]);
138+
139+
expect(result).toEqual([{ state: "LOADED", value: 1000 }, { state: "LOADING" }]);
140+
141+
store.dispatch({
142+
type: "API_CALL_SUCCESS",
143+
call_key: activePremiumsCallKey,
144+
value: `ret${currencyAddress}activePremiums`,
145+
timestamp: new Date().getTime(),
146+
});
147+
148+
result = selectAPICallMultiple(store.getState().APIReducer, [
149+
{ apiName: "apy", args: [currencyAddress] },
150+
{ apiName: "activePremiums", args: [currencyAddress] },
151+
]);
152+
153+
expect(result).toEqual([
154+
{ state: "LOADED", value: 1000 },
155+
{ state: "LOADED", value: `ret${currencyAddress}activePremiums` },
156+
]);
157+
});
158+
});

src/store/api/selectors.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import _ from "lodash";
22
import { createSelector } from "reselect";
33
import { getAPIFn } from "../../package-index";
44

5+
const EMPTYSTATE = {};
6+
57
const getCalls = (state) => state.calls;
68
const getCallMetadata = (state) => state.call_metadata;
7-
const getCallKey = (__, apiName, args) => {
9+
export const getCallKey = (__, apiName, args) => {
810
let api = getAPIFn(apiName);
911
return api.urlFunction(...(args || []));
1012
};
@@ -37,6 +39,6 @@ export const selectAPICallState = createSelector(
3739

3840
export const selectAPICallMultiple = createSelector([getCalls, getCallKeys], (calls, callKeys) =>
3941
_.map(callKeys, (callKey) => {
40-
return calls[callKey] === undefined ? {} : { value: calls[callKey].value, state: calls[callKey].state };
42+
return !calls || calls[callKey] === undefined ? EMPTYSTATE : calls[callKey];
4143
})
4244
);

0 commit comments

Comments
 (0)