~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


ce83445f pyrossh

2 years ago
fix example
example/src/components/Todo/Todo.jsx DELETED
@@ -1,71 +0,0 @@
1
- import { useState, useCallback } from "react";
2
- import "./Todo.css";
3
-
4
- const Todo = ({ item, updateMutation, deleteMutation }) => {
5
- const [editing, setEditing] = useState(false);
6
- const doSave = useCallback(() => {
7
- if (!input.current) return;
8
- setBusy(true);
9
- save(item, input.current.value, item.completed);
10
- }, [item]);
11
- const cancelEdit = useCallback(() => {
12
- if (!input.current) return;
13
- setEditing(false);
14
- input.current.value = item.text;
15
- }, []);
16
- const doDelete = useCallback(() => {
17
- const yes = confirm("Are you sure you want to delete this item?");
18
- if (!yes) return;
19
- setBusy(true);
20
- save(item, null, item.completed);
21
- }, [item]);
22
- const doSaveCompleted = useCallback(
23
- (completed) => {
24
- setBusy(true);
25
- save(item, item.text, completed);
26
- },
27
- [item],
28
- );
29
- return (
30
- <li className="todo">
31
- {!editing && (
32
- <>
33
- <input type="checkbox" />
34
- <div class="text">
35
- <p>{item.text}</p>
36
- <p className="timestamp">{item.createdAt}</p>
37
- </div>
38
- <button className="edit-button" title="Edit">
39
- ✏️
40
- </button>
41
- <button class="delete-button" title="Delete">
42
- 🗑️
43
- </button>
44
- </>
45
- )}
46
- {editing && (
47
- <>
48
- <input class="border rounded w-full py-2 px-3 mr-4" defaultValue={item.text} />
49
- <button
50
- class="p-2 rounded mr-2 disabled:opacity-50"
51
- title="Save"
52
- onClick={doSave}
53
- disabled={busy}
54
- >
55
- 💾
56
- </button>
57
- <button
58
- class="p-2 rounded disabled:opacity-50"
59
- title="Cancel"
60
- onClick={cancelEdit}
61
- disabled={busy}
62
- >
63
- 🚫
64
- </button>
65
- </>
66
- )}
67
- </li>
68
- );
69
- };
70
-
71
- export default Todo;
example/src/pages/app.jsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { Suspense } from "react";
1
2
  import { SSRProvider } from "react-aria";
2
3
  import Layout from "@/components/Layout/Layout";
3
4
  import "./normalize.css";
@@ -7,7 +8,11 @@ import "./app.css";
7
8
  export default function App({ children }) {
8
9
  return (
9
10
  <SSRProvider>
11
+ <Layout>
10
- <Layout>{children}</Layout>
12
+ <Suspense fallback={<p>Loading...</p>}>
13
+ {children}
14
+ </Suspense>
15
+ </Layout>
11
16
  </SSRProvider>
12
17
  );
13
- }
18
+ }
example/src/pages/spectrum.css CHANGED
@@ -373,9 +373,8 @@
373
373
  --focus-ring-color: slateblue;
374
374
  --invalid-color: var(--spectrum-global-color-red-600);
375
375
  display: flex;
376
- flex-direction: column;
377
- width: fit-content;
378
376
  flex: 1;
377
+ flex-direction: column;
379
378
 
380
379
  .react-aria-Input {
381
380
  margin: 0;
example/src/{components/Todo → pages/todos}/Todo.css RENAMED
@@ -16,6 +16,17 @@
16
16
  margin-left: 0.5rem;
17
17
  }
18
18
 
19
+ & form {
20
+ display: flex;
21
+ flex: 1;
22
+
23
+ & .text-input {
24
+ display: flex;
25
+ flex: 1;
26
+ margin-right: 0.5rem;
27
+ }
28
+ }
29
+
19
30
  & .timestamp {
20
31
  line-height: 2;
21
32
  opacity: 0.5;
example/src/pages/todos/Todo.jsx ADDED
@@ -0,0 +1,73 @@
1
+ import { useState } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { TextField, Input } from "react-aria-components";
4
+ import { useRpcCache, useMutation } from "edge-city";
5
+ import { updateTodo, deleteTodo } from "@/services/todos.service";
6
+ import "./Todo.css";
7
+
8
+ const Todo = ({ item }) => {
9
+ const [editing, setEditing] = useState(false);
10
+ const {
11
+ register,
12
+ handleSubmit,
13
+ reset,
14
+ } = useForm();
15
+ const { invalidate } = useRpcCache("todos");
16
+ const updateMutation = useMutation(async ({ text }) => {
17
+ await updateTodo({ id: item.id, text, completed: item.completed });
18
+ await invalidate();
19
+ setEditing(false);
20
+ });
21
+ const deleteMutation = useMutation(async (id) => {
22
+ await deleteTodo(id);
23
+ await invalidate();
24
+ });
25
+ return (
26
+ <li className="todo" style={{ opacity: deleteMutation.isMutating ? 0.5 : 1 }}>
27
+ {!editing && (
28
+ <>
29
+ <input type="checkbox" />
30
+ <div class="text">
31
+ <p>{item.text}</p>
32
+ <p className="timestamp">{item.createdAt}</p>
33
+ </div>
34
+ <button className="edit-button" title="Edit" onClick={() => setEditing(true)}>
35
+ ✏️
36
+ </button>
37
+ <button class="delete-button" title="Delete" onClick={() => deleteMutation.mutate(item.id)}>
38
+ 🗑️
39
+ </button>
40
+ </>
41
+ )}
42
+ {editing && (
43
+ <form onSubmit={handleSubmit(updateMutation.mutate)}>
44
+ <TextField isRequired isReadOnly={updateMutation.isMutating}>
45
+ <Input {...register("text")} defaultValue={item.text} />
46
+ {/* {err?.text && <p>{err.text._errors[0]}</p>} */}
47
+ </TextField>
48
+ <button
49
+ type="submit"
50
+ className="edit-button"
51
+ title="Save"
52
+ disabled={updateMutation.isMutating}
53
+ >
54
+ 💾
55
+ </button>
56
+ <button
57
+ className="delete-button"
58
+ title="Cancel"
59
+ onClick={() => {
60
+ reset({ text: item.text });
61
+ setEditing(false);
62
+ }}
63
+ disabled={updateMutation.isMutating}
64
+ >
65
+ 🚫
66
+ </button>
67
+ </form>
68
+ )}
69
+ </li>
70
+ );
71
+ };
72
+
73
+ export default Todo;
example/src/pages/todos/TodoList.jsx ADDED
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { useQuery } from "edge-city";
3
+ import { getTodos } from "@/services/todos.service";
4
+ import Todo from "./Todo";
5
+ import "./page.css";
6
+
7
+ export default function TodoList({ isMutating }) {
8
+ const { data, isRefetching } = useQuery("todos", () => getTodos());
9
+ return (
10
+ <>
11
+ {isMutating || isRefetching ? <p>Loading...</p> : null}
12
+ <ul>
13
+ {data.map((item) => (
14
+ <Todo
15
+ key={item.id}
16
+ item={item}
17
+ />
18
+ ))}
19
+ </ul>
20
+ </>
21
+ );
22
+ }
example/src/pages/todos/page.css CHANGED
@@ -19,7 +19,7 @@
19
19
  font-size: 0.875rem;
20
20
  line-height: 1.25rem;
21
21
  opacity: 0.5;
22
- margin-top: 1rem;
22
+ margin-top: 0.5rem;
23
23
  }
24
24
 
25
25
  & form {
example/src/pages/todos/page.jsx CHANGED
@@ -1,34 +1,29 @@
1
- import React from "react";
1
+ import { Suspense } from "react";
2
+ import { ErrorBoundary } from "react-error-boundary";
2
3
  import { Helmet } from "react-helmet-async";
3
- import { useQuery, useMutation } from "edge-city";
4
+ import { useMutation, useRpcCache } from "edge-city";
4
5
  import { useForm } from "react-hook-form";
5
6
  import { Button, TextField, Input } from "react-aria-components";
6
- import Todo from "@/components/Todo/Todo";
7
+ import TodoList from "./TodoList";
7
- import { getTodos, createTodo, updateTodo, deleteTodo } from "@/services/todos.service";
8
+ import { createTodo } from "@/services/todos.service";
8
9
  import "./page.css";
9
10
 
10
11
  export default function Page() {
12
+ const {
13
+ register,
14
+ handleSubmit,
15
+ reset,
16
+ formState: { errors },
17
+ } = useForm();
11
- const { data, refetch } = useQuery("todos", () => getTodos());
18
+ const { invalidate } = useRpcCache("todos");
12
19
  const { mutate, isMutating, err } = useMutation(async ({ text }) => {
13
20
  await createTodo({
14
21
  text,
15
22
  completed: false,
16
23
  });
17
- await refetch();
24
+ await invalidate();
18
- });
19
- const updateMutation = useMutation(async ({ text, completed }) => {
20
- await updateTodo({ text, completed });
21
- await refetch();
25
+ reset();
22
26
  });
23
- const deleteMutation = useMutation(async (id) => {
24
- await deleteTodo(id);
25
- await refetch();
26
- });
27
- const {
28
- register,
29
- handleSubmit,
30
- formState: { errors },
31
- } = useForm();
32
27
  return (
33
28
  <div className="todos-page">
34
29
  <h1 className="title">Todo List</h1>
@@ -46,16 +41,11 @@ export default function Page() {
46
41
  Add
47
42
  </Button>
48
43
  </form>
49
- <ul>
44
+ <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
50
- {data.map((item) => (
45
+ <Suspense fallback={<p>Loading...</p>}>
51
- <Todo
52
- key={item.id}
53
- item={item}
54
- updateMutation={updateMutation}
46
+ <TodoList isMutating={isMutating} />
55
- deleteMutation={deleteMutation}
56
- />
57
- ))}
58
- </ul>
47
+ </Suspense>
48
+ </ErrorBoundary>
59
49
  </div>
60
50
  </div>
61
51
  );
example/src/services/todos.service.js CHANGED
@@ -1,4 +1,4 @@
1
- import { eq, asc } from "drizzle-orm";
1
+ import { eq, desc } from "drizzle-orm";
2
2
  import { boolean, date, pgTable, serial, text } from "drizzle-orm/pg-core";
3
3
  import { z } from "zod";
4
4
 
@@ -16,16 +16,27 @@ export const createSchema = z.object({
16
16
  });
17
17
 
18
18
  const updateSchema = z.object({
19
+ id: z.number().positive().int("must be an integer"),
19
20
  text: z.string().nonempty("please enter some text"),
20
21
  completed: z.boolean(),
21
22
  });
22
23
 
23
24
  export const getTodos = async () => {
25
+ await new Promise((resolve) => {
26
+ setTimeout(() => {
27
+ resolve();
28
+ }, 2000)
29
+ });
24
- return await db.select().from(todos).orderBy(asc(todos.id));
30
+ return await db.select().from(todos).orderBy(desc(todos.id));
25
31
  };
26
32
 
27
33
  /** @param {z.infer<typeof createSchema>} params */
28
34
  export const createTodo = async (params) => {
35
+ await new Promise((resolve) => {
36
+ setTimeout(() => {
37
+ resolve();
38
+ }, 2000)
39
+ });
29
40
  const item = createSchema.parse(params);
30
41
  item.createdAt = new Date();
31
42
  return await db.insert(todos).values(item).returning();
index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, {
2
- Suspense, createContext, useContext, useState, useEffect, useTransition, useCallback
2
+ Suspense, createContext, useContext, useState, useEffect, useTransition, useCallback, useRef
3
3
  } from "react";
4
4
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
5
5
  import { HelmetProvider } from 'react-helmet-async';
@@ -40,20 +40,22 @@ export const useInvalidate = () => {
40
40
 
41
41
  export const useRpcCache = (k) => {
42
42
  const ctx = useContext(RpcContext);
43
- const [_, rerender] = useState(false);
44
- const get = () => ctx[k]
43
+ if (isClient() && !ctx.subs[k]) {
45
- const set = (v) => {
44
+ ctx.subs[k] = new Set();
46
- ctx[k] = v;
47
- rerender((c) => !c);
48
45
  }
46
+ const get = () => ctx.data[k]
49
- const invalidate = () => {
47
+ const set = (v) => {
50
- delete ctx[k];
48
+ ctx.data[k] = v;
51
- rerender((c) => !c);
52
49
  }
50
+ const invalidate = () => Promise.all(Array.from(ctx.subs[k]).map((cb) => cb()))
53
51
  return {
54
52
  get,
55
53
  set,
56
54
  invalidate,
55
+ onInvalidated: (cb) => {
56
+ ctx.subs[k].add(cb)
57
+ return () => ctx.subs[k].delete(cb);
58
+ }
57
59
  }
58
60
  }
59
61
 
@@ -66,7 +68,7 @@ export const useRpcCache = (k) => {
66
68
  export const useQuery = (key, fn) => {
67
69
  const [isRefetching, setIsRefetching] = useState(false);
68
70
  const [err, setErr] = useState(null);
69
- const cache = useRpcCache(key);
71
+ const cache = useRpcCache(key, fn);
70
72
  const refetch = useCallback(async () => {
71
73
  try {
72
74
  setIsRefetching(true);
@@ -79,6 +81,9 @@ export const useQuery = (key, fn) => {
79
81
  setIsRefetching(false);
80
82
  }
81
83
  }, [fn]);
84
+ useEffect(() => {
85
+ return cache.onInvalidated(refetch);
86
+ }, [key])
82
87
  const value = cache.get();
83
88
  if (value) {
84
89
  if (value instanceof Promise) {
@@ -132,19 +137,15 @@ export const RouterProvider = ({ router, history, rpcContext, helmetContext, App
132
137
  history,
133
138
  params: page.params || {},
134
139
  },
140
+ children: _jsx(Suspense, {
141
+ fallback: _jsx("div", {}, "Routing...."),
135
- children: _jsx(HelmetProvider, {
142
+ children: _jsx(HelmetProvider, {
136
- context: helmetContext,
143
+ context: helmetContext,
137
- children: _jsx(RpcContext.Provider, {
144
+ children: _jsx(RpcContext.Provider, {
138
- value: rpcContext,
145
+ value: rpcContext,
139
- children: _jsx(ErrorBoundary, {
140
- onError: (err) => console.log(err),
141
- fallback: _jsx("p", {}, "Oops something went wrong"),
142
- children: _jsx(Suspense, {
143
- fallback: _jsx("p", {}, "Loading..."),
144
- children: _jsx(App, {
146
+ children: _jsx(App, {
145
- children: _jsx(page, {}),
147
+ children: _jsx(page, {}),
146
- })
148
+ })
147
- }),
148
149
  }),
149
150
  }),
150
151
  }),
renderPage.js CHANGED
@@ -115,7 +115,7 @@ const renderPage = async (Page, App, req) => {
115
115
  });
116
116
  const jsScript = url.pathname === "/" ? "/index" : url.pathname;
117
117
  const helmetContext = {};
118
- const rpcContext = {};
118
+ const rpcContext = { data: {}, subs: {} };
119
119
  const stream = await render(
120
120
  _jsxs("html", {
121
121
  lang: "en",