Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tannaurus committed Sep 29, 2024
1 parent e3772c3 commit 199d0b4
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 19 deletions.
214 changes: 214 additions & 0 deletions loading_suggestion.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
diff --git a/src/App.tsx b/src/App.tsx
index 2b4f35f..6ef7f04 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -48,7 +48,7 @@ export function App() {
* This custom hook takes our token and fetches the data for our list.
* Check ./api/firestore.js for its implementation.
*/
- const data = useShoppingListData(listPath);
+ const listState = useShoppingListData(listPath);

return (
<>
@@ -66,11 +66,13 @@ export function App() {
<Route element={<ProtectRoute user={user} redirectPath="/" />}>
<Route
path="/list"
- element={<List data={data} listPath={listPath} />}
+ element={<List listState={listState} listPath={listPath} />}
/>
<Route
path="/manage-list"
- element={<ManageList listPath={listPath} data={data || []} />}
+ element={
+ <ManageList listPath={listPath} listState={listState || []} />
+ }
/>
</Route>

diff --git a/src/api/firebase.ts b/src/api/firebase.ts
index 8d7a524..4f2ce8e 100644
--- a/src/api/firebase.ts
+++ b/src/api/firebase.ts
@@ -103,6 +103,17 @@ const ListItemModel = t.type({

export type ListItem = t.TypeOf<typeof ListItemModel>;

+export interface ListDataLoading {
+ type: "loading";
+}
+
+export interface ListData {
+ type: "data";
+ items: ListItem[];
+}
+
+export type ListState = ListDataLoading | ListData;
+
/**
* A custom hook that subscribes to a shopping list in our Firestore database
* and returns new data whenever the list changes.
@@ -111,10 +122,19 @@ export type ListItem = t.TypeOf<typeof ListItemModel>;
export function useShoppingListData(listPath: string | null) {
// Start with an empty array for our data.
/** @type {import('firebase/firestore').DocumentData[]} */
- const [data, setData] = useState<ListItem[]>([]);
+ const [state, setState] = useState<ListDataLoading | ListData>({
+ type: "loading",
+ });

useEffect(() => {
- if (!listPath) return;
+ if (!listPath) {
+ // If we don't have a listPath, there's inherently no data: no need to switch to a loading state.
+ setState({ type: "data", items: [] });
+ return;
+ }
+
+ // If the listPath has changed, anticipating some loading.
+ setState({ type: "loading" });

// When we get a listPath, we use it to subscribe to real-time updates
// from Firestore.
@@ -131,6 +151,8 @@ export function useShoppingListData(listPath: string | null) {

const decoded = ListItemModel.decode(item);
if (isLeft(decoded)) {
+ // If we failed to decode the data, we don't want to leave the app in a loading state that will never resolve.
+ setState({ type: "data", items: [] });
throw Error(
`Could not validate data: ${PathReporter.report(decoded).join("\n")}`,
);
@@ -139,13 +161,16 @@ export function useShoppingListData(listPath: string | null) {
return decoded.right;
});

- // Update our React state with the new data.
- setData(nextData);
+ // Once we've received and deserialize the data, we can update our state.
+ setState({
+ type: "data",
+ items: nextData,
+ });
});
}, [listPath]);

// Return the data so it can be used by our React components.
- return data;
+ return state;
}

// Designed to replace Firestore's User type in most contexts.
diff --git a/src/views/authenticated/List.tsx b/src/views/authenticated/List.tsx
index b48a442..64e2c31 100644
--- a/src/views/authenticated/List.tsx
+++ b/src/views/authenticated/List.tsx
@@ -1,25 +1,28 @@
import { useState, useMemo } from "react";
import { ListItemCheckBox } from "../../components/ListItem";
import { FilterListInput } from "../../components/FilterListInput";
-import { ListItem, comparePurchaseUrgency } from "../../api";
+import { ListState, comparePurchaseUrgency } from "../../api";
import { useNavigate } from "react-router-dom";

interface Props {
- data: ListItem[];
+ listState: ListState;
listPath: string | null;
}

-export function List({ data: unfilteredListItems, listPath }: Props) {
+export function List({ listState, listPath }: Props) {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState<string>("");

const filteredListItems = useMemo(() => {
- return unfilteredListItems
+ if (listState.type === "loading") {
+ return [];
+ }
+ return listState.items
.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
.sort(comparePurchaseUrgency);
- }, [searchTerm, unfilteredListItems]);
+ }, [searchTerm, listState]);

const Header = () => {
return (
@@ -33,8 +36,19 @@ export function List({ data: unfilteredListItems, listPath }: Props) {
return <Header />;
}

+ if (listState.type === "loading") {
+ return (
+ <>
+ <Header />
+ <section>
+ <h3>Loading your list...</h3>
+ </section>
+ </>
+ );
+ }
+
// Early return if the list is empty
- if (unfilteredListItems.length === 0) {
+ if (listState.items.length === 0) {
return (
<>
<Header />
@@ -61,7 +75,7 @@ export function List({ data: unfilteredListItems, listPath }: Props) {
<Header />
<div>
<section>
- {unfilteredListItems.length > 0 && (
+ {listState.items.length > 0 && (
<FilterListInput
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
diff --git a/src/views/authenticated/ManageList.tsx b/src/views/authenticated/ManageList.tsx
index 9cdd3cf..0aeefc8 100644
--- a/src/views/authenticated/ManageList.tsx
+++ b/src/views/authenticated/ManageList.tsx
@@ -1,13 +1,13 @@
import { AddItemForm } from "../../components/forms/AddItemForm";
import ShareListForm from "../../components/forms/ShareListForm";
-import { ListItem } from "../../api";
+import { ListState } from "../../api";

interface Props {
- data: ListItem[];
+ listState: ListState;
listPath: string | null;
}

-export function ManageList({ listPath, data }: Props) {
+export function ManageList({ listPath, listState }: Props) {
const Header = () => {
return (
<p>
@@ -20,10 +20,21 @@ export function ManageList({ listPath, data }: Props) {
return <Header />;
}

+ if (listState.type === "loading") {
+ return (
+ <>
+ <Header />
+ <section>
+ <h3>Loading your list...</h3>
+ </section>
+ </>
+ );
+ }
+
return (
<div>
<Header />
- <AddItemForm listPath={listPath} data={data || []} />
+ <AddItemForm listPath={listPath} data={listState.items} />
<ShareListForm listPath={listPath} />
</div>
);
8 changes: 5 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function App() {
* This custom hook takes our token and fetches the data for our list.
* Check ./api/firestore.js for its implementation.
*/
const data = useShoppingListData(listPath);
const listState = useShoppingListData(listPath);

return (
<>
Expand All @@ -66,11 +66,13 @@ export function App() {
<Route element={<ProtectRoute user={user} redirectPath="/" />}>
<Route
path="/list"
element={<List data={data} listPath={listPath} />}
element={<List listState={listState} listPath={listPath} />}
/>
<Route
path="/manage-list"
element={<ManageList listPath={listPath} data={data || []} />}
element={
<ManageList listPath={listPath} listState={listState || []} />
}
/>
</Route>

Expand Down
35 changes: 30 additions & 5 deletions src/api/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ const ListItemModel = t.type({

export type ListItem = t.TypeOf<typeof ListItemModel>;

export interface ListDataLoading {
type: "loading";
}

export interface ListData {
type: "data";
items: ListItem[];
}

export type ListState = ListDataLoading | ListData;

/**
* A custom hook that subscribes to a shopping list in our Firestore database
* and returns new data whenever the list changes.
Expand All @@ -111,10 +122,19 @@ export type ListItem = t.TypeOf<typeof ListItemModel>;
export function useShoppingListData(listPath: string | null) {
// Start with an empty array for our data.
/** @type {import('firebase/firestore').DocumentData[]} */
const [data, setData] = useState<ListItem[]>([]);
const [state, setState] = useState<ListDataLoading | ListData>({
type: "loading",
});

useEffect(() => {
if (!listPath) return;
if (!listPath) {
// If we don't have a listPath, there's inherently no data: no need to switch to a loading state.
setState({ type: "data", items: [] });
return;
}

// If the listPath has changed, anticipating some loading.
setState({ type: "loading" });

// When we get a listPath, we use it to subscribe to real-time updates
// from Firestore.
Expand All @@ -131,6 +151,8 @@ export function useShoppingListData(listPath: string | null) {

const decoded = ListItemModel.decode(item);
if (isLeft(decoded)) {
// If we failed to decode the data, we don't want to leave the app in a loading state that will never resolve.
setState({ type: "data", items: [] });
throw Error(
`Could not validate data: ${PathReporter.report(decoded).join("\n")}`,
);
Expand All @@ -139,13 +161,16 @@ export function useShoppingListData(listPath: string | null) {
return decoded.right;
});

// Update our React state with the new data.
setData(nextData);
// Once we've received and deserialize the data, we can update our state.
setState({
type: "data",
items: nextData,
});
});
}, [listPath]);

// Return the data so it can be used by our React components.
return data;
return state;
}

// Designed to replace Firestore's User type in most contexts.
Expand Down
28 changes: 21 additions & 7 deletions src/views/authenticated/List.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { useState, useMemo } from "react";
import { ListItemCheckBox } from "../../components/ListItem";
import { FilterListInput } from "../../components/FilterListInput";
import { ListItem, comparePurchaseUrgency } from "../../api";
import { ListState, comparePurchaseUrgency } from "../../api";
import { useNavigate } from "react-router-dom";

interface Props {
data: ListItem[];
listState: ListState;
listPath: string | null;
}

export function List({ data: unfilteredListItems, listPath }: Props) {
export function List({ listState, listPath }: Props) {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState<string>("");

const filteredListItems = useMemo(() => {
return unfilteredListItems
if (listState.type === "loading") {
return [];
}
return listState.items
.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
.sort(comparePurchaseUrgency);
}, [searchTerm, unfilteredListItems]);
}, [searchTerm, listState]);

const Header = () => {
return (
Expand All @@ -33,8 +36,19 @@ export function List({ data: unfilteredListItems, listPath }: Props) {
return <Header />;
}

if (listState.type === "loading") {
return (
<>
<Header />
<section>
<h3>Loading your list...</h3>
</section>
</>
);
}

// Early return if the list is empty
if (unfilteredListItems.length === 0) {
if (listState.items.length === 0) {
return (
<>
<Header />
Expand All @@ -61,7 +75,7 @@ export function List({ data: unfilteredListItems, listPath }: Props) {
<Header />
<div>
<section>
{unfilteredListItems.length > 0 && (
{listState.items.length > 0 && (
<FilterListInput
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
Expand Down
Loading

0 comments on commit 199d0b4

Please sign in to comment.