Skip to content

Commit 60b8500

Browse files
authored
Cross-tab cache invalidation (#104)
Fixes #57 - Use RTK's cache invalidation for gear and people install of manually refetching data on update - Use `BroadcastChannel` to invalidate data across browser tabs and windows
1 parent b65b4cb commit 60b8500

File tree

15 files changed

+100
-65
lines changed

15 files changed

+100
-65
lines changed

src/pages/Approvals/AddNewApproval.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState } from "react";
2-
import { useDispatch } from "react-redux";
32
import { useLocation, useNavigate } from "react-router-dom";
43

54
import {
@@ -9,7 +8,8 @@ import {
98
import { APIError as APIErrorClass } from "src/apiClient/client";
109
import { APIErrorType } from "src/apiClient/types";
1110
import { useSetPageTitle } from "src/hooks";
12-
import { gearDbApi } from "src/redux/api";
11+
import { TagType } from "src/redux/api";
12+
import { invalidateCache } from "src/redux/store";
1313

1414
import { AddNewApprovalForm } from "./AddNewApprovalForm";
1515

@@ -22,12 +22,11 @@ export default function AddNewApproval() {
2222
const searchParams = new URLSearchParams(location.search);
2323
const personId = searchParams.get("personId");
2424

25-
const dispatch = useDispatch();
2625
const onSubmit = (args: CreateNewApprovalArgs) => {
2726
createNewApproval(args)
2827
.then(() => {
2928
setError(undefined);
30-
dispatch(gearDbApi.util.invalidateTags(["Approvals"]));
29+
invalidateCache([TagType.Approvals]);
3130
if (personId != null) {
3231
navigate(`/people/${personId}?tab=approvals`);
3332
} else {

src/pages/Gear/GearInfoPanel.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import { Link } from "react-router-dom";
44
import type { GearSummary } from "src/apiClient/gear";
55
import { newApprovalUI } from "src/featureFlags";
66
import { fmtAmount } from "src/lib/fmtNumber";
7+
import { TagType } from "src/redux/api";
78
import { useConfig } from "src/redux/hooks";
9+
import { invalidateCache } from "src/redux/store";
810

911
import { GearItemEditForm } from "./GearItemEditForm";
1012
import { GearStatus } from "./GearStatus";
1113
import { GearStatusForm, GearStatusFormType } from "./GearStatusForm";
1214

13-
type Props = { gearItem: GearSummary; refreshGear: () => void };
15+
type Props = { gearItem: GearSummary };
1416

15-
export function GearInfoPanel({ gearItem, refreshGear }: Props) {
17+
export function GearInfoPanel({ gearItem }: Props) {
1618
const [formToShow, setFormToShow] = useState<GearStatusFormType>(
1719
GearStatusFormType.none,
1820
);
@@ -78,7 +80,9 @@ export function GearInfoPanel({ gearItem, refreshGear }: Props) {
7880
<GearItemEditForm
7981
gearItem={gearItem}
8082
closeForm={() => setEditing(false)}
81-
refreshGear={refreshGear}
83+
refreshGear={() => {
84+
invalidateCache([TagType.GearItems]);
85+
}}
8286
/>
8387
)}
8488

@@ -108,7 +112,7 @@ export function GearInfoPanel({ gearItem, refreshGear }: Props) {
108112
formType={formToShow}
109113
gearItem={gearItem}
110114
onChange={() => {
111-
refreshGear();
115+
invalidateCache([TagType.GearItems]);
112116
setFormToShow(GearStatusFormType.none);
113117
}}
114118
/>

src/pages/Gear/GearItemPage.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useParams } from "react-router-dom";
33
import { addNote } from "src/apiClient/gear";
44
import { Notes } from "src/components/Notes";
55
import { useSetPageTitle } from "src/hooks";
6-
import { useGetGearItemQuery } from "src/redux/api";
6+
import { TagType, useGetGearItemQuery } from "src/redux/api";
7+
import { invalidateCache } from "src/redux/store";
78

89
import { GearInfoPanel } from "./GearInfoPanel";
910
import { GearPicture } from "./GearPicture";
@@ -12,19 +13,23 @@ import { GearRentalsHistory } from "./GearRentalsHistory";
1213
export default function GearItemPage() {
1314
const gearId = useParams<{ gearId: string }>().gearId!;
1415
useSetPageTitle(gearId);
15-
const { data: gearItem, refetch: refreshGear } = useGetGearItemQuery(gearId);
16+
const { data: gearItem } = useGetGearItemQuery(gearId);
1617

1718
if (gearItem == null) {
1819
return null;
1920
}
2021
return (
2122
<div className="row">
2223
<div className="col-12 col-md-5 p-2">
23-
<GearInfoPanel gearItem={gearItem} refreshGear={refreshGear} />
24-
<GearPicture gearItem={gearItem} refreshGear={refreshGear} />
24+
<GearInfoPanel gearItem={gearItem} />
25+
<GearPicture gearItem={gearItem} />
2526
<Notes
2627
notes={gearItem.notes}
27-
onAdd={(note) => addNote(gearId, note).then(refreshGear)}
28+
onAdd={(note) =>
29+
addNote(gearId, note).then(() =>
30+
invalidateCache([TagType.GearItems]),
31+
)
32+
}
2833
/>
2934
</div>
3035
<div className="col-12 col-md-7 p-2">

src/pages/Gear/GearPicture.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import { useState } from "react";
44
import styled from "styled-components";
55

66
import { GearItem } from "src/apiClient/gear";
7+
import { TagType } from "src/redux/api";
8+
import { invalidateCache } from "src/redux/store";
79

810
import { PicturePickerModal } from "./PicturePickerModal";
911
import { PicturePlaceholder } from "./PicturePlaceholder";
1012

1113
type Props = {
1214
gearItem: GearItem;
13-
refreshGear: () => void;
1415
};
1516

16-
export function GearPicture({ gearItem, refreshGear }: Props) {
17+
export function GearPicture({ gearItem }: Props) {
1718
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
1819

1920
return (
@@ -40,7 +41,7 @@ export function GearPicture({ gearItem, refreshGear }: Props) {
4041
isOpen={isModalOpen}
4142
close={() => setIsModalOpen(false)}
4243
item={gearItem}
43-
refreshGear={refreshGear}
44+
refreshGear={() => invalidateCache([TagType.GearItems])}
4445
/>
4546
)}
4647
</>

src/pages/People/PeoplePage/PersonPageContext.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import React, { useContext, useState } from "react";
55
import { RenterApproval } from "src/apiClient/approvals";
66
import { GearSummary } from "src/apiClient/gear";
77
import { checkoutGear, Person, Rental, returnGear } from "src/apiClient/people";
8+
import { TagType } from "src/redux/api";
9+
import { invalidateCache } from "src/redux/store";
810

911
import { ItemToPurchase } from "../types";
1012

@@ -14,7 +16,6 @@ type PersonPageContextType = ReturnType<typeof useMakePersonPageContext>;
1416

1517
type Props = {
1618
person: Person;
17-
refreshPerson: () => void;
1819
approvals: RenterApproval[];
1920
};
2021

@@ -44,7 +45,7 @@ export function usePersonPageContext() {
4445
return context;
4546
}
4647

47-
function useMakePersonPageContext({ person, refreshPerson, approvals }: Props) {
48+
function useMakePersonPageContext({ person, approvals }: Props) {
4849
const checkoutBasketBase = useBasket<GearSummary>();
4950
const returnBasketBase = useBasket<Rental>();
5051
const purchaseBasket = useBasket<ItemToPurchase>();
@@ -115,7 +116,6 @@ function useMakePersonPageContext({ person, refreshPerson, approvals }: Props) {
115116
person,
116117
approvals,
117118
isApproved,
118-
refreshPerson,
119119
purchaseBasket,
120120
returnBasket: {
121121
...returnBasketBase,
@@ -139,7 +139,7 @@ function useMakePersonPageContext({ person, refreshPerson, approvals }: Props) {
139139
).then(() => {
140140
returnBasketBase.clear();
141141
purchaseBasket.clear();
142-
refreshPerson();
142+
invalidateCache([TagType.People]);
143143
});
144144
},
145145
},
@@ -149,7 +149,7 @@ function useMakePersonPageContext({ person, refreshPerson, approvals }: Props) {
149149
const gearIDs = map(checkoutBasketBase.items, "id");
150150
return checkoutGear(person.id, gearIDs).then(() => {
151151
checkoutBasketBase.clear();
152-
refreshPerson();
152+
invalidateCache([TagType.People]);
153153
});
154154
},
155155
},

src/pages/People/PersonPage.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { useParams } from "react-router-dom";
44
import { addNote, archiveNote } from "src/apiClient/people";
55
import { Notes } from "src/components/Notes";
66
import { useSetPageTitle } from "src/hooks";
7-
import { useGetPersonQuery, useGetRenterApprovalsQuery } from "src/redux/api";
7+
import {
8+
TagType,
9+
useGetPersonQuery,
10+
useGetRenterApprovalsQuery,
11+
} from "src/redux/api";
12+
import { invalidateCache } from "src/redux/store";
813

914
import { BuyGear } from "./BuyGear";
1015
import { CheckoutStaging } from "./CheckoutStaging";
@@ -22,7 +27,7 @@ import { ReturnStaging } from "./ReturnStaging";
2227

2328
export function PersonPage() {
2429
const personId = useParams<{ personId: string }>().personId!;
25-
const { data: person, refetch: refreshPerson } = useGetPersonQuery(personId);
30+
const { data: person } = useGetPersonQuery(personId);
2631
const { data: approvalResult } = useGetRenterApprovalsQuery({
2732
personID: personId,
2833
past: false,
@@ -33,7 +38,6 @@ export function PersonPage() {
3338
return (
3439
<PersonPageContextProvider
3540
person={person}
36-
refreshPerson={refreshPerson}
3741
// NOTE: This doesn't handle pagination for now, which could be a problem if someone has 50+ active approvals. Unlikely.
3842
approvals={approvalResult?.results ?? []}
3943
>
@@ -45,14 +49,8 @@ export function PersonPage() {
4549
function PersonPageInner() {
4650
const [tab, setTab] = useTab();
4751

48-
const {
49-
person,
50-
refreshPerson,
51-
checkoutBasket,
52-
returnBasket,
53-
purchaseBasket,
54-
isApproved,
55-
} = usePersonPageContext();
52+
const { person, checkoutBasket, returnBasket, purchaseBasket, isApproved } =
53+
usePersonPageContext();
5654

5755
useSetPageTitle(person ? `${person.firstName} ${person.lastName} ` : "");
5856

@@ -75,10 +73,14 @@ function PersonPageInner() {
7573
<Notes
7674
notes={person.notes}
7775
onAdd={(note) => {
78-
return addNote(person.id, note).then(refreshPerson);
76+
return addNote(person.id, note).then(() =>
77+
invalidateCache([TagType.People]),
78+
);
7979
}}
8080
onArchive={(noteId) => {
81-
archiveNote(person.id, noteId).then(refreshPerson);
81+
archiveNote(person.id, noteId).then(() =>
82+
invalidateCache([TagType.People]),
83+
);
8284
}}
8385
/>
8486
{!isEmpty(checkoutBasket.items) && (

src/pages/People/PersonProfile/FrequentFlyerForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { useState } from "react";
44
import DatePicker from "react-datepicker";
55

66
import { addFFChecks, Person } from "src/apiClient/people";
7-
import { useGetPersonQuery } from "src/redux/api";
7+
import { TagType } from "src/redux/api";
8+
import { invalidateCache } from "src/redux/store";
89

910
import { getNextExpirationDate } from "./utils";
1011

@@ -14,7 +15,6 @@ type Props = {
1415
};
1516

1617
export function FrequentFlyerForm({ person, onClose }: Props) {
17-
const { refetch: refreshPerson } = useGetPersonQuery(String(person.id));
1818
const initial = getNextExpirationDate();
1919
const [date, setDate] = useState<Date>(initial);
2020
const [checkNumber, setCheckNumber] = useState<string>("");
@@ -49,7 +49,7 @@ export function FrequentFlyerForm({ person, onClose }: Props) {
4949
onClick={(evt) => {
5050
evt.preventDefault();
5151
addFFChecks(person.id, date, checkNumber).then(() => {
52-
refreshPerson();
52+
invalidateCache([TagType.People]);
5353
onClose();
5454
});
5555
}}

src/pages/People/PersonProfile/MembershipForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import DatePicker from "react-datepicker";
55

66
import { addMembership, Person } from "src/apiClient/people";
77
import { Select } from "src/components/Inputs/Select";
8-
import { useGetAffiliationsQuery, useGetPersonQuery } from "src/redux/api";
8+
import { TagType, useGetAffiliationsQuery } from "src/redux/api";
9+
import { invalidateCache } from "src/redux/store";
910

1011
import { getNextExpirationDate } from "./utils";
1112

@@ -15,7 +16,6 @@ type Props = {
1516
};
1617

1718
export function MembershipForm({ person, onClose }: Props) {
18-
const { refetch: refreshPerson } = useGetPersonQuery(String(person.id));
1919
const { data: affiliations = [] } = useGetAffiliationsQuery();
2020
const affiliationOptions = affiliations.map(({ id, name }) => ({
2121
value: id,
@@ -63,7 +63,7 @@ export function MembershipForm({ person, onClose }: Props) {
6363
return;
6464
}
6565
addMembership(person.id, date, membershipType).then(() => {
66-
refreshPerson();
66+
invalidateCache([TagType.People]);
6767
onClose();
6868
});
6969
}}

src/pages/People/PersonProfile/MitocCreditForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { useState } from "react";
22

33
import { addMitocCredit, Person } from "src/apiClient/people";
44
import { NumberField } from "src/components/Inputs/NumberField";
5-
import { useGetPersonQuery } from "src/redux/api";
5+
import { TagType } from "src/redux/api";
6+
import { invalidateCache } from "src/redux/store";
67

78
type Props = {
89
person: Person;
910
onClose: () => void;
1011
};
1112

1213
export function MitocCreditForm({ person, onClose }: Props) {
13-
const { refetch: refreshPerson } = useGetPersonQuery(String(person.id));
1414
const [amount, setAmount] = useState<number | null>(15);
1515
return (
1616
<div>
@@ -21,7 +21,7 @@ export function MitocCreditForm({ person, onClose }: Props) {
2121
return;
2222
}
2323
addMitocCredit(person.id, amount).then(() => {
24-
refreshPerson();
24+
invalidateCache([TagType.People]);
2525
onClose();
2626
});
2727
}}

src/pages/People/PersonProfile/PeopleGroups.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { useState } from "react";
33

44
import { PeopleGroup, Person, updatePersonGroups } from "src/apiClient/people";
55
import { GroupSelect } from "src/components/GroupSelect";
6+
import { TagType } from "src/redux/api";
67
import { useCurrentUser, usePermissions } from "src/redux/auth";
8+
import { invalidateCache } from "src/redux/store";
79

810
type Props = {
911
person: Person;
10-
refreshPerson: () => void;
1112
};
1213

13-
export default function PeopleGroups({ person, refreshPerson }: Props) {
14+
export default function PeopleGroups({ person }: Props) {
1415
const [showGroupsForm, setShowGroupForms] = useState<boolean>(false);
1516
const { user } = useCurrentUser();
1617
const { isOfficer } = usePermissions();
@@ -53,15 +54,14 @@ export default function PeopleGroups({ person, refreshPerson }: Props) {
5354
<PeopleGroupsForm
5455
isOfficer={isOfficer}
5556
person={person}
56-
refreshPerson={refreshPerson}
5757
closeForm={() => setShowGroupForms(false)}
5858
/>
5959
);
6060
}
6161

6262
function PeopleGroupsForm({
6363
person,
64-
refreshPerson,
64+
6565
closeForm,
6666
isOfficer,
6767
}: Props & { isOfficer?: boolean; closeForm: () => void }) {
@@ -72,7 +72,7 @@ function PeopleGroupsForm({
7272
person.id,
7373
groups.map((g) => g.id),
7474
).then(() => {
75-
refreshPerson();
75+
invalidateCache([TagType.People]);
7676
closeForm();
7777
});
7878

0 commit comments

Comments
 (0)