~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


f07784df pyrossh

2 years ago
Add spinner
example/src/components/Layout/Layout.css CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  & a {
15
15
  margin-right: 20px;
16
+ padding: 0.25rem;
16
17
  }
17
18
  }
18
19
 
example/src/components/Spinner/Spinner.css ADDED
@@ -0,0 +1,40 @@
1
+ .spinner-container {
2
+ display: flex;
3
+ flex: 1;
4
+ align-items: center;
5
+ justify-content: center;
6
+ padding: 1rem;
7
+ }
8
+
9
+ .spinner {
10
+ animation: rotate 2s linear infinite;
11
+ width: 50px;
12
+ height: 50px;
13
+ }
14
+
15
+ .spinner-path {
16
+ stroke: #2563eb;
17
+ stroke-linecap: round;
18
+ animation: dash 1.5s ease-in-out infinite;
19
+ }
20
+
21
+ @keyframes rotate {
22
+ 100% {
23
+ transform: rotate(360deg);
24
+ }
25
+ }
26
+
27
+ @keyframes dash {
28
+ 0% {
29
+ stroke-dasharray: 1, 150;
30
+ stroke-dashoffset: 0;
31
+ }
32
+ 50% {
33
+ stroke-dasharray: 90, 150;
34
+ stroke-dashoffset: -35;
35
+ }
36
+ 100% {
37
+ stroke-dasharray: 90, 150;
38
+ stroke-dashoffset: -124;
39
+ }
40
+ }
example/src/components/Spinner/Spinner.jsx ADDED
@@ -0,0 +1,11 @@
1
+ import "./Spinner.css";
2
+
3
+ export default function Spinner() {
4
+ return (
5
+ <div className="spinner-container">
6
+ <svg className="spinner" viewBox="0 0 50 50">
7
+ <circle className="spinner-path" cx="25" cy="25" r="20" fill="none" strokeWidth="5"></circle>
8
+ </svg>
9
+ </div>
10
+ )
11
+ }
example/src/pages/app.css CHANGED
@@ -19,3 +19,7 @@ h1 {
19
19
  margin-top: 1rem;
20
20
  margin-bottom: 1rem;
21
21
  }
22
+
23
+ a:hover {
24
+ text-decoration: underline;
25
+ }
example/src/pages/todos/Todo.jsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useState } from "react";
2
2
  import { useForm } from "react-hook-form";
3
3
  import { TextField, Input } from "react-aria-components";
4
- import { useRpcCache, useMutation } from "edge-city";
4
+ import { cache, useMutation } from "edge-city";
5
5
  import { updateTodo, deleteTodo } from "@/services/todos.service";
6
6
  import "./Todo.css";
7
7
 
@@ -12,36 +12,35 @@ const Todo = ({ item }) => {
12
12
  handleSubmit,
13
13
  reset,
14
14
  } = useForm();
15
- const { invalidate } = useRpcCache("todos");
16
15
  const updateMutation = useMutation(async ({ text }) => {
17
16
  await updateTodo({ id: item.id, text, completed: item.completed });
18
- await invalidate();
17
+ await cache.invalidate("todos", false);
19
18
  setEditing(false);
20
19
  });
21
20
  const deleteMutation = useMutation(async (id) => {
22
21
  await deleteTodo(id);
23
- await invalidate();
22
+ await cache.invalidate("todos", false);
24
23
  });
25
24
  return (
26
- <li className="todo" style={{ opacity: deleteMutation.isMutating ? 0.5 : 1 }}>
25
+ <li className="todo" style={{ opacity: deleteMutation.isMutating || updateMutation.isMutating ? 0.5 : 1 }}>
27
26
  {!editing && (
28
27
  <>
29
28
  <input type="checkbox" />
30
- <div class="text">
29
+ <div className="text">
31
30
  <p>{item.text}</p>
32
31
  <p className="timestamp">{item.createdAt}</p>
33
32
  </div>
34
33
  <button className="edit-button" title="Edit" onClick={() => setEditing(true)}>
35
34
  ✏️
36
35
  </button>
37
- <button class="delete-button" title="Delete" onClick={() => deleteMutation.mutate(item.id)}>
36
+ <button className="delete-button" title="Delete" onClick={() => deleteMutation.mutate(item.id)}>
38
37
  🗑️
39
38
  </button>
40
39
  </>
41
40
  )}
42
41
  {editing && (
43
42
  <form onSubmit={handleSubmit(updateMutation.mutate)}>
44
- <TextField isRequired isReadOnly={updateMutation.isMutating}>
43
+ <TextField isRequired isReadOnly={updateMutation.isMutating} isDisabled={updateMutation.isMutating}>
45
44
  <Input {...register("text")} defaultValue={item.text} />
46
45
  {/* {err?.text && <p>{err.text._errors[0]}</p>} */}
47
46
  </TextField>
example/src/pages/todos/TodoList.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { useQuery } from "edge-city";
3
3
  import { getTodos } from "@/services/todos.service";
4
+ import Spinner from "@/components/Spinner/Spinner";
4
5
  import Todo from "./Todo";
5
6
  import "./page.css";
6
7
 
@@ -8,7 +9,7 @@ export default function TodoList({ isMutating }) {
8
9
  const { data, isRefetching } = useQuery("todos", () => getTodos());
9
10
  return (
10
11
  <>
11
- {isMutating || isRefetching ? <p>Loading...</p> : null}
12
+ {isMutating || isRefetching ? <Spinner /> : null}
12
13
  <ul>
13
14
  {data.map((item) => (
14
15
  <Todo
example/src/pages/todos/page.jsx CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Suspense } from "react";
2
2
  import { ErrorBoundary } from "react-error-boundary";
3
3
  import { Helmet } from "react-helmet-async";
4
- import { useMutation, useRpcCache } from "edge-city";
4
+ import { cache, useMutation } from "edge-city";
5
5
  import { useForm } from "react-hook-form";
6
6
  import { Button, TextField, Input } from "react-aria-components";
7
+ import Spinner from "@/components/Spinner/Spinner";
7
8
  import TodoList from "./TodoList";
8
9
  import { createTodo } from "@/services/todos.service";
9
10
  import "./page.css";
@@ -15,13 +16,12 @@ export default function Page() {
15
16
  reset,
16
17
  formState: { errors },
17
18
  } = useForm();
18
- const { invalidate } = useRpcCache("todos");
19
19
  const { mutate, isMutating, err } = useMutation(async ({ text }) => {
20
20
  await createTodo({
21
21
  text,
22
22
  completed: false,
23
23
  });
24
- await invalidate();
24
+ await cache.invalidate("todos");
25
25
  reset();
26
26
  });
27
27
  return (
@@ -41,8 +41,8 @@ export default function Page() {
41
41
  Add
42
42
  </Button>
43
43
  </form>
44
- <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
44
+ <ErrorBoundary onError={(err) => console.log("err", err)} fallback={<p>Oops something went wrong</p>}>
45
- <Suspense fallback={<p>Loading...</p>}>
45
+ <Suspense fallback={<Spinner />}>
46
46
  <TodoList isMutating={isMutating} />
47
47
  </Suspense>
48
48
  </ErrorBoundary>
index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import React, {
2
- Suspense, createContext, useContext, useState, useEffect, useTransition, useCallback, useRef
2
+ Suspense, createContext, useContext, useState, useEffect, useTransition, useCallback
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';
6
- import { ErrorBoundary } from "react-error-boundary";
7
6
  import { createBrowserHistory } from "history";
8
7
  import { createRouter } from "radix3";
9
8
  import routemap from '/routemap.json' assert {type: 'json'};
@@ -24,38 +23,18 @@ export const defineRpc = (serviceName) => async (params = {}) => {
24
23
  return await res.json();
25
24
  }
26
25
 
27
- export const RpcContext = createContext(undefined);
28
-
29
- // global way to refresh maybe without being tied to a hook like refetch
30
- export const useInvalidate = () => {
26
+ export const cache = {
31
- const ctx = useContext(RpcContext);
27
+ get: (k) => globalThis._EDGE_DATA_.data[k],
32
- return (regex) => {
28
+ set: (k, v) => {
33
- Object.keys(ctx)
34
- .filter((k) => regex.test(k))
29
+ globalThis._EDGE_DATA_.data[k] = v;
35
- .forEach((k) => {
36
- delete ctx[k];
37
- });
38
- }
30
+ },
39
- }
40
-
41
- export const useRpcCache = (k) => {
42
- const ctx = useContext(RpcContext);
43
- if (isClient() && !ctx.subs[k]) {
44
- ctx.subs[k] = new Set();
45
- }
46
- const get = () => ctx.data[k]
47
- const set = (v) => {
48
- ctx.data[k] = v;
49
- }
50
- const invalidate = () => Promise.all(Array.from(ctx.subs[k]).map((cb) => cb()))
31
+ invalidate: (k, setRefetch) => Promise.all(Array.from(globalThis._EDGE_DATA_.subs[k]).map((cb) => cb(setRefetch))),
51
- return {
52
- get,
53
- set,
54
- invalidate,
55
- onInvalidated: (cb) => {
32
+ subscribe: (k, cb) => {
56
- ctx.subs[k].add(cb)
33
+ if (!globalThis._EDGE_DATA_.subs[k]) {
57
- return () => ctx.subs[k].delete(cb);
34
+ globalThis._EDGE_DATA_.subs[k] = new Set();
58
35
  }
36
+ globalThis._EDGE_DATA_.subs[k].add(cb)
37
+ return () => globalThis._EDGE_DATA_.subs[k].delete(cb);
59
38
  }
60
39
  }
61
40
 
@@ -66,25 +45,31 @@ export const useRpcCache = (k) => {
66
45
  * @returns
67
46
  */
68
47
  export const useQuery = (key, fn) => {
48
+ const [, toggle] = useState(false);
69
49
  const [isRefetching, setIsRefetching] = useState(false);
70
50
  const [err, setErr] = useState(null);
71
- const cache = useRpcCache(key, fn);
72
- const refetch = useCallback(async () => {
51
+ const refetch = useCallback(async (setRefetch = true) => {
73
52
  try {
53
+ if (setRefetch) {
74
- setIsRefetching(true);
54
+ setIsRefetching(true);
55
+ }
75
56
  setErr(null);
76
- cache.set(await fn());
57
+ cache.set(key, await fn());
77
58
  } catch (err) {
78
59
  setErr(err);
79
60
  throw err;
80
61
  } finally {
62
+ if (setRefetch) {
81
- setIsRefetching(false);
63
+ setIsRefetching(false);
64
+ } else {
65
+ toggle((v) => !v);
66
+ }
82
67
  }
83
68
  }, [fn]);
84
69
  useEffect(() => {
85
- return cache.onInvalidated(refetch);
70
+ return cache.subscribe(key, refetch);
86
71
  }, [key])
87
- const value = cache.get();
72
+ const value = cache.get(key);
88
73
  if (value) {
89
74
  if (value instanceof Promise) {
90
75
  throw value;
@@ -93,8 +78,8 @@ export const useQuery = (key, fn) => {
93
78
  }
94
79
  return { data: value, isRefetching, err, refetch };
95
80
  }
96
- cache.set(fn().then((v) => cache.set(v)));
81
+ cache.set(key, fn().then((v) => cache.set(key, v)));
97
- throw cache.get();
82
+ throw cache.get(key);
98
83
  }
99
84
 
100
85
  export const useMutation = (fn) => {
@@ -120,7 +105,7 @@ export const useMutation = (fn) => {
120
105
  }
121
106
 
122
107
  export const RouterContext = createContext(undefined);
123
- export const RouterProvider = ({ router, history, rpcContext, helmetContext, App }) => {
108
+ export const RouterProvider = ({ router, history, helmetContext, App }) => {
124
109
  const [_, startTransition] = useTransition();
125
110
  const [pathname, setPathname] = useState(history.location.pathname);
126
111
  const page = router.lookup(pathname) || router.lookup("/_404");
@@ -141,12 +126,9 @@ export const RouterProvider = ({ router, history, rpcContext, helmetContext, App
141
126
  fallback: _jsx("div", {}, "Routing...."),
142
127
  children: _jsx(HelmetProvider, {
143
128
  context: helmetContext,
144
- children: _jsx(RpcContext.Provider, {
145
- value: rpcContext,
146
- children: _jsx(App, {
129
+ children: _jsx(App, {
147
- children: _jsx(page, {}),
130
+ children: _jsx(page, {}),
148
- })
131
+ })
149
- }),
150
132
  }),
151
133
  }),
152
134
  })
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 = { data: {}, subs: {} };
118
+ globalThis._EDGE_DATA_ = { data: {}, subs: {} };
119
119
  const stream = await render(
120
120
  _jsxs("html", {
121
121
  lang: "en",
@@ -135,7 +135,6 @@ const renderPage = async (Page, App, req) => {
135
135
  _jsx(RouterProvider, {
136
136
  history,
137
137
  router,
138
- rpcContext,
139
138
  helmetContext,
140
139
  App,
141
140
  }),
@@ -150,7 +149,7 @@ const renderPage = async (Page, App, req) => {
150
149
  .join(''),
151
150
  injectOnEnd: () => {
152
151
  return ''
153
- + `<script>window._EDGE_DATA_ = ${JSON.stringify(rpcContext)};</script>`
152
+ + `<script>globalThis._EDGE_DATA_ = ${JSON.stringify(globalThis._EDGE_DATA_)};</script>`
154
153
  + `<script type="module" src="/js${jsScript}.js?hydrate=true" defer></script>`
155
154
  }
156
155
  });