~repos /edge-city

#react#js#ssr

git clone https://pyrossh.dev/repos/edge-city.git

edge-city is a next level meta-framework for react that runs only on edge runtimes


b43c8da5 Peter John

2 years ago
Improve example and useRpc hook
bun.lockb CHANGED
Binary file
package.json CHANGED
@@ -16,9 +16,10 @@
16
16
  "next-auth": "^4.22.1",
17
17
  "normalize.css": "^8.0.1",
18
18
  "react": "18.2.0",
19
+ "react-aria-components": "1.0.0-alpha.3",
19
- "react-dom": "^18.2.0",
20
+ "react-dom": "18.2.0",
20
- "react-error-boundary": "^4.0.4",
21
+ "react-error-boundary": "4.0.4",
21
- "react-helmet-async": "^1.3.0",
22
+ "react-helmet-async": "1.3.0",
22
23
  "sql-highlight": "^4.3.2"
23
24
  },
24
25
  "devDependencies": {
pages/todos/page.css CHANGED
@@ -1,4 +1,71 @@
1
1
  body {
2
2
  padding: 10px;
3
3
  background-color: turquoise;
4
+ }
5
+
6
+
7
+ :root {
8
+ --spectrum-alias-border-color: black;
9
+ --spectrum-global-color-gray-50: white;
10
+ }
11
+
12
+
13
+ .react-aria-TextField {
14
+ --field-border: var(--spectrum-alias-border-color);
15
+ --field-border-disabled: var(--spectrum-alias-border-color-disabled);
16
+ --field-background: var(--spectrum-global-color-gray-50);
17
+ --text-color: var(--spectrum-alias-text-color);
18
+ --text-color-disabled: var(--spectrum-alias-text-color-disabled);
19
+ --focus-ring-color: slateblue;
20
+ --invalid-color: var(--spectrum-global-color-red-600);
21
+
22
+ display: flex;
23
+ flex-direction: column;
24
+ width: fit-content;
25
+
26
+ .react-aria-Input {
27
+ padding: 0.286rem;
28
+ margin: 0;
29
+ border: 1px solid var(--field-border);
30
+ border-radius: 6px;
31
+ background: var(--field-background);
32
+ font-size: 1.143rem;
33
+ color: var(--text-color);
34
+
35
+ &[aria-invalid] {
36
+ border-color: var(--invalid-color);
37
+ }
38
+
39
+ &:focus {
40
+ outline: none;
41
+ border-color: var(--focus-ring-color);
42
+ box-shadow: 0 0 0 1px var(--focus-ring-color);
43
+ }
44
+
45
+ &:disabled {
46
+ border-color: var(--field-border-disabled);
47
+ color: var(--text-color-disabled);
48
+ }
49
+ }
50
+
51
+ [slot=description] {
52
+ font-size: 12px;
53
+ }
54
+
55
+ [slot=errorMessage] {
56
+ font-size: 12px;
57
+ color: var(--invalid-color);
58
+ }
59
+ }
60
+
61
+ @media (forced-colors: active) {
62
+ .react-aria-TextField {
63
+ --field-border: ButtonBorder;
64
+ --field-border-disabled: GrayText;
65
+ --field-background: Field;
66
+ --text-color: FieldText;
67
+ --text-color-disabled: GrayText;
68
+ --focus-ring-color: Highlight;
69
+ --invalid-color: LinkText;
70
+ }
4
71
  }
pages/todos/page.jsx CHANGED
@@ -1,19 +1,42 @@
1
- import React, { Suspense } from 'react';
1
+ import React, { Suspense, useState } from 'react';
2
2
  import { Helmet } from 'react-helmet-async';
3
3
  import { useRpc } from "parotta/runtime";
4
4
  import Todo from "@/components/Todo/Todo";
5
+ import { TextField, Label, Input } from 'react-aria-components';
6
+ import { Button } from 'react-aria-components';
5
- import { getTodos } from "@/services/todos.service";
7
+ import { getTodos, createTodo } from "@/services/todos.service";
6
8
  import Layout from '@/components/Layout/Layout';
7
9
  import "./page.css";
8
10
 
9
11
  const TodoList = () => {
10
- const { data } = useRpc(getTodos, {});
12
+ const { data, isRefetching, refetch } = useRpc(getTodos, {});
13
+ const [text, setText] = useState();
14
+ const onSubmit = async () => {
15
+ await createTodo({
16
+ text,
17
+ completed: false,
18
+ createdAt: new Date(),
19
+ })
20
+ await refetch();
21
+ }
11
22
  return (
23
+ <div>
12
- <ul>
24
+ <ul>
13
- {data.map((item) => (
25
+ {data.map((item) => (
14
- <Todo key={item.id} todo={item} />
26
+ <Todo key={item.id} todo={item} />
15
- ))}
27
+ ))}
28
+ {isRefetching && <div>
29
+ <p>Refetching...</p>
30
+ </div>}
16
- </ul>
31
+ </ul>
32
+ <div>
33
+ <TextField isRequired>
34
+ <Label>Text (required)</Label>
35
+ <Input value={text} onChange={(e) => setText(e.target.value)} />
36
+ </TextField>
37
+ <Button onPress={onSubmit}>Add Todo</Button>
38
+ </div>
39
+ </div>
17
40
  )
18
41
  }
19
42
 
parotta/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, {
2
- Suspense, createElement, createContext, useContext, useState, useEffect, useTransition
2
+ Suspense, createElement, createContext, useContext, useState, useEffect, useTransition, useCallback
3
3
  } from "react";
4
4
  import { HelmetProvider } from 'react-helmet-async';
5
5
  import { ErrorBoundary } from "react-error-boundary";
@@ -7,6 +7,7 @@ import { ErrorBoundary } from "react-error-boundary";
7
7
  export const domain = () => typeof window !== 'undefined' ? window.origin : "http://0.0.0.0:3000";
8
8
 
9
9
  export const rpc = (serviceName) => async (params = {}) => {
10
+ console.log('serviceName', serviceName);
10
11
  const res = await fetch(`${domain()}/services/${serviceName}`, {
11
12
  method: "POST",
12
13
  headers: {
@@ -19,20 +20,26 @@ export const rpc = (serviceName) => async (params = {}) => {
19
20
  }
20
21
 
21
22
  export const RpcContext = createContext(undefined);
23
+ // global way to refresh maybe without being tied to a hook like refetch
24
+ // const invalidate = (regex) => {
25
+ // Object.keys(ctx)
26
+ // .filter((k) => regex.test(k))
27
+ // .forEach((k) => {
28
+ // fetchData(k).then((v) => set(k, v));
29
+ // });
30
+ // }
31
+
22
- export const useCache = () => {
32
+ export const useCache = (k) => {
23
- const [_, rerender] = useState(false);
24
33
  const ctx = useContext(RpcContext);
34
+ const [_, rerender] = useState(false);
25
- const get = (k) => ctx[k]
35
+ const get = () => ctx[k]
26
- const set = (k, v) => {
36
+ const set = (v) => {
27
37
  ctx[k] = v;
28
38
  rerender((c) => !c);
29
39
  }
30
- const invalidate = (regex) => {
40
+ const invalidate = () => {
31
- Object.keys(ctx)
32
- .filter((k) => regex.test(k))
41
+ ctx[k] = undefined;
33
- .forEach((k) => {
42
+ rerender((c) => !c);
34
- fetchData(k).then((v) => set(k, v));
35
- });
36
43
  }
37
44
  return {
38
45
  get,
@@ -41,20 +48,40 @@ export const useCache = () => {
41
48
  }
42
49
  }
43
50
 
51
+ /**
52
+ *
53
+ * @param {*} fn
54
+ * @param {*} params
55
+ * @returns
56
+ */
44
57
  export const useRpc = (fn, params) => {
58
+ const [isRefetching, setIsRefetching] = useState(false);
45
- const cache = useCache();
59
+ const [err, setErr] = useState(null);
46
60
  const key = `${fn.name}:${JSON.stringify(params)}`;
61
+ const cache = useCache(key);
62
+ const refetch = useCallback(async () => {
63
+ try {
64
+ setIsRefetching(true);
65
+ setErr(null);
66
+ cache.set(await fn(params));
67
+ } catch (err) {
68
+ setErr(err);
69
+ throw err;
70
+ } finally {
71
+ setIsRefetching(false);
72
+ }
73
+ }, [key])
47
- const value = cache.get(key);
74
+ const value = cache.get();
48
75
  if (value) {
49
76
  if (value instanceof Promise) {
50
77
  throw value;
51
78
  } else if (value instanceof Error) {
52
79
  throw value;
53
80
  }
54
- return { data: value, cache };
81
+ return { data: value, isRefetching, err, refetch };
55
82
  }
56
- cache.set(key, fn(params).then((v) => cache.set(key, v)));
83
+ cache.set(fn(params).then((v) => cache.set(v)));
57
- throw cache.get(key);
84
+ throw cache.get();
58
85
  }
59
86
 
60
87
  export const RouterContext = createContext(undefined);
parotta/server.js CHANGED
@@ -149,7 +149,7 @@ const renderPage = async (url) => {
149
149
  }, {})
150
150
  const components = mapDeps("components");
151
151
  const importMap = {
152
- "radix3": `https://esm.sh/radix3`,
152
+ "radix3": `https://esm.sh/radix3@1.0.1`,
153
153
  "history": "https://esm.sh/history@5.3.0",
154
154
  "react": `https://esm.sh/react@18.2.0${devTag}`,
155
155
  // TODO: need to remove this in prod
@@ -239,9 +239,9 @@ const renderJs = async (srcFile) => {
239
239
  addRpcImport = true;
240
240
  const [importName, serviceName] = ln.match(/\@\/services\/(.*)\.service/);
241
241
  const funcsText = ln.replace(`from "${importName}"`, "").replace("import", "").replace("{", "").replace("}", "").replace(";", "");
242
- const funcsName = funcsText.trim().split(",");
242
+ const funcsName = funcsText.split(",");
243
243
  funcsName.forEach((fnName) => {
244
- lines.push(`const ${fnName} = rpc("${serviceName}/${fnName}")`);
244
+ lines.push(`const ${fnName} = rpc("${serviceName}/${fnName.trim()}")`);
245
245
  })
246
246
  }
247
247
  })
@@ -295,7 +295,6 @@ const server = async (req) => {
295
295
  return renderJs(path.join(process.cwd(), url.pathname));
296
296
  }
297
297
  const match = serverRouter.lookup(url.pathname);
298
- // TODO: maybe remove this as renderPage would handle it in clientRouter
299
298
  if (match && !match.key.includes("/_")) {
300
299
  if (match.file) {
301
300
  return sendFile(path.join(process.cwd(), `/static${match.file}`));
services/todos.service.js CHANGED
@@ -5,6 +5,11 @@ export const getTodos = async () => {
5
5
  return await db.select().from(todos).orderBy(asc(todos.id));
6
6
  }
7
7
 
8
+ /**
9
+ *
10
+ * @param {typeof todos} item
11
+ * @returns
12
+ */
8
13
  export const createTodo = async (item) => {
9
14
  return await db.insert(todos).values(item).returning();
10
15
  }