Skip to content

Commit

Permalink
Replace useFetch with plain useEffect statements. I have struggled to…
Browse files Browse the repository at this point in the history
… figure out how to compose custom hooks that have effects.
  • Loading branch information
dgroomes committed Jan 7, 2024
1 parent b2f7210 commit a07cc97
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 239 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ General clean-ups, todos and things I wish to implement for this project:
and not using semicolons.
* [x] DONE (fixed, but `useFetch` now doesn't make sense. How do people do this? Just ignore unmounts for the clean up function?)
Defect: validation fetch request is getting cancelled prematurely. My `useFetch` must be buggy.
* [x] DONE Stop setting state from render function. During the redux conversion, I started getting warnings about setting
state from the render function. Totally (well 80%) makes sense to me, so I'll fix it. This is part of the process of
grokking React. The trouble is in `useToken`. At this time, it's time to drop `useFetch` which I had previously marked
as deprecated. It's so hard to make this work.


## Finished Wish List Items
Expand Down Expand Up @@ -171,6 +175,7 @@ General clean-ups, todos and things I wish to implement for this project:
* [x] DONE Upgrade dependencies.
* [x] DONE (GraphiQL defines fonts in data URLs) Why are there HTTP request failures to download fonts? E.g. `data:font/woff2;base64,` etc. This happens when
serving but not in the production app.
* [ ] More robust error handling. We at least want to model basic error states and propage a basic message.
## Reference
Expand Down
16 changes: 9 additions & 7 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {configureStore} from "@reduxjs/toolkit";
import { configureStore } from "@reduxjs/toolkit";
import monolithicReducer from "./monolithicSlice";
// @ts-ignore
import {customizeWebpackConfigForDevelopment} from "redux-config-customizer";
import { customizeWebpackConfigForDevelopment } from "redux-config-customizer";

export const store = configureStore(customizeWebpackConfigForDevelopment({
reducer: {
monolithic: monolithicReducer,
}
}));
export const store = configureStore(
customizeWebpackConfigForDevelopment({
reducer: {
monolithic: monolithicReducer,
},
}),
);

// I don't understand how this works, but this is needed when using Redux Toolkit with TypeScript.
// See https://redux.js.org/tutorials/typescript-quick-start
Expand Down
149 changes: 0 additions & 149 deletions src/useFetch.ts

This file was deleted.

141 changes: 58 additions & 83 deletions src/useToken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useFetch } from "./useFetch";
import { TokenState, logger } from "./code";
import { logger, TokenState } from "./code";

const log = logger("useToken");

Expand All @@ -12,13 +11,14 @@ const log = logger("useToken");
* gets used. If not, the hook waits on the user to enter a token in the UI. After a token is restored or entered, the
* hook validates it using the GitHub API. The hook gives the validated token the backend via IPC to store it.
*
* This is not working well as a custom React hook. It might be better as a component or just externalized to non-React
* code. I've struggled and learned some things. But I'm still not clear on the right way to do this.
* Modeling "restoring" on {@link TokenState} is not symmetrical to modeling "isFetching" inside this hook. Consider
* refactoring this.
*/
export function useToken(): [TokenState, Dispatch<SetStateAction<TokenState>>] {
log("Invoked.");
const [token, setToken] = useState<TokenState>("restoring");
const [shouldStoreAfterValidation, setShouldStoreAfterValidation] = useState(false);
const [isFetching, setIsFetching] = useState(false);
if (typeof token === "object") {
log({ kind: token.kind });
} else if (token === "empty") {
Expand All @@ -45,31 +45,68 @@ export function useToken(): [TokenState, Dispatch<SetStateAction<TokenState>>] {
});
}, [token]);

let fetchParams: { input: RequestInfo | URL; init?: RequestInit } | "no-op";

if (typeof token === "object" && (token.kind === "entered" || token.kind === "restored")) {
const query = `
useEffect(() => {
if (typeof token === "object" && (token.kind === "entered" || token.kind === "restored")) {
setIsFetching(true);
const query = `
query {
viewer {
login
}
}
`;
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `bearer ${token.token}`,
},
body: JSON.stringify({ query }),
};
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `bearer ${token.token}`,
},
body: JSON.stringify({ query }),
};

const fetchParams = { input: "https://api.github.com/graphql", init: options };
log("Executing the 'fetch' request...");
fetch(fetchParams.input, fetchParams.init)
.then((res) => {
log("The 'fetch' request completed.");
if (res.status == 401) {
log("GitHub API responded with 401 Unauthorized. The token is invalid.");
setToken({
kind: "invalid",
token: token.token,
});
return;
}

return res.json();
})

fetchParams = { input: "https://api.github.com/graphql", init: options };
} else {
fetchParams = "no-op";
}
.then((json) => {
const login = json["data"]["viewer"]["login"];
log("The token was found to be valid. GitHub login: ", login);

// Note: this is verbose and circuitous...
if (shouldStoreAfterValidation) {
setToken({
kind: "storing",
token: token.token,
login: login,
});
} else {
setToken({
kind: "valid",
token: token.token,
login: login,
});
}
})
.catch((err) => {
log("The 'fetch' request failed.", { err });
// TODO Set token state to an error so that the using component can render user-friendly error message
});
}
}, [token]);

const fetched = useFetch(fetchParams);
useEffect(() => {
if (typeof token === "object" && token.kind === "storing") {
window.api
Expand All @@ -86,67 +123,5 @@ export function useToken(): [TokenState, Dispatch<SetStateAction<TokenState>>] {
}
}, [token]);

if (fetched === "in-flight") {
log("The 'useFetch' hook is in-flight. We need to wait for it.");
return [token, setToken];
}

if (fetched === "untriggered") {
log("The 'useFetch' hook is untriggered. We need to wait until its triggered.");
return [token, setToken];
}

if (typeof token === "object" && (token.kind === "valid" || token.kind === "invalid")) {
log("The token has already finished the validation process.");
return [token, setToken];
}

if (typeof token === "object" && token.kind === "storing") {
log("The token is storing.");
return [token, setToken];
}

if (typeof token !== "object" || !(token.kind === "entered" || token.kind === "restored")) {
// This is nasty. I'm having difficulty composing my code with 'useEffect'. It's hard to satisfy the requirement of "hooks must be called at the top level and never in a conditional".
throw new Error(
"This should never happen. The token should be an object with kind 'entered' or 'restored' at this point.",
);
}

if (fetched instanceof Error) {
setToken({
kind: "invalid",
token: token.token,
});
return [token, setToken];
}

const { status, json } = fetched;

if (status == 401) {
log("GitHub API responded with 401 Unauthorized. The token is invalid.");
setToken({
kind: "invalid",
token: token.token,
});
} else {
const login = json["data"]["viewer"]["login"];
log("The token was found to be valid. GitHub login: ", login);

// Note: this is verbose and circuitous...
if (shouldStoreAfterValidation) {
setToken({
kind: "storing",
token: token.token,
login: login,
});
} else {
setToken({
kind: "valid",
token: token.token,
login: login,
});
}
}
return [token, setToken];
}

0 comments on commit a07cc97

Please sign in to comment.