asmer 3 mesi fa
parent
commit
4d5970651e
6 ha cambiato i file con 303 aggiunte e 57 eliminazioni
  1. 162 0
      README.md
  2. 2 56
      src/App.tsx
  3. 2 0
      src/lib/pub/index.ts
  4. 59 0
      src/lib/pub/query/index.ts
  5. 1 1
      src/lib/router/index.tsx
  6. 77 0
      src/test/route.tsx

+ 162 - 0
README.md

@@ -2,3 +2,165 @@
 
 [Description](http://doc.a-level.com.ua/react-layers)
 
+# React Layers
+**NIH-syndrom again**
+
+## Goal
+
+Create layered **React** microframework with reasonable architecture and minimal
+boilerplate.
+
+## Architecture
+
+Layers, one-by-one from deepest layer (global state management, server queries) to directly **View** (react) layer:
+
+### Pub (and Sub)
+
+Minimalistic pub/sub with recursive support builded on top of [Javascript Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
+```javascript
+const pub = createPub()
+pub.subscribe((state, key, oldValue, newValue) => console.log(state, key, oldValue, newValue))
+
+pub.a = 5
+pub.arr = ['foo', 'bar']
+pub.arr[1] = 'baz'
+```
+
+On any change of keys in objects `subscriber` called. You can listen to updates in any nested object. Listening on upper level hears also nested updates. 
+This way you can achieve global state tree without boilerplate, but this way requires attention to what you do with state.
+
+#### React Listening Hook
+
+`usePub`, obviously. Causes react component update on changing of pub. To minimize overhead, `usePub` can be used for nested pubs, instead of root state.
+
+```javascript
+const arrPub = usePub(pub.arr)
+```
+
+Hook returns pub, so `arrPub === pub.arr`, but component updating when pub changes.
+
+#### Network Requests/Promises
+
+**Pub** can be easily used for tracking promises. 
+!!! (error)
+Be careful, **storing promises in global state causes memory leaks**
+!!!
+
+##### How
+0. creating nested **Pub** with structure like
+```javascript
+{
+    promiseName1: {status, payload, error}
+    promiseName2: {status, payload, error}
+}
+```
+2. Writing function, which have `promiseName` and  exactly promise as argument and updates promise pub branch when promise updating its state (**PENDING => FULFILLED** or 
+**PENDING => REJECTED**), storing `status`, `payload` and `error` in promise pub named branch
+3. ~~rtk-query~~:
+4. 
+
+    - composing functions: query function, which returns promise and "thunk", which passed promise life through pub. Something like `createAsyncThunk`, or `createQuery`, with next parameters:
+        - `promiseName`.
+        - `queryFunc` - function, which returns promises.
+
+
+    or even **carrying**:
+        1. `createQueryPub` creates pub with promises, some basic options passed (like cache timeout and probably queryFunc) and returns `createQuery` or object with functions from step 3 if queryFunc passed on first step into createQueryPub
+        2. `createQuery(queryFunc)` returns function, which:
+        3.  receives `promiseName`, returns object with functions:
+            - `query`, which accepts arguments and passes it to `queryFunc`
+            - `usePromiseNameQuery`/`usePromiseNameMutation` like in `rtk-query`
+            - and so... (no ideas right now, but something about atomic actions/force query/invalidate cache)
+    
+
+### Actions/Controllers/Thunks
+To avoid random messy access to global state, some functions which works with are welcome, but they aren't necessary. 
+
+### Routing
+Instead of moving Routing on React level, making routing ~~great again~~ separated from React Markup.
+
+#### Why
+- _Because_
+- Routing describes _page_ content overall, but not how it will exactly looks (React does)
+- Routing describes **request** to backend, regardless of look.
+
+
+#### Two layers of routing:
+- address routing
+- hash routing
+
+#### Conclusion
+Routing configuration should be separated from React markup, and store next information:
+- **Route Template** like `/page/:id`
+- **Route Name** which probably same as one of promise names
+- **Query Func** or even **Query Params** to make query with **Pub**. **Query Func** can be made very easy with **GraphQL**. **Query Func** gets
+    all information about route (or, at least, params from route) as function parameters to make query. Usually **Query Func** is just thunk with promise for Pub.
+- **React Page Component**, which should be rendered for this **Route**. Probably with **MetaPlate** to avoid _props drilling, `useHuy`, `map`, props waterfall, `usePizda`, `map`_
+    and so
+- **Some private routing configuration**, like `fallback` (address to go, if private route not accessible) and `roles` - list of user roles, which can
+    visit this private page. Some HOC needed to provide current user roles list (usually from global state/jwt) to be matched with `roles` props
+
+#### some ideas and thoughts
+- address routing (from domain till ?
+- hash routing (from # till end of address)
+
+
+possible routing configuration:
+- list style, as JSON or JSX (like <Route .../>)
+- tree style, potentially faster (but it doesn't matters on client side)
+```javascript
+const treeRouteConfig = {
+    //root page:
+    "/": PageMain,
+    //login
+    '/login': PageLogin,
+    //admin part with subroutes
+    '/admin': {
+        "/": PageAdminMain, // /admin/
+        "/users": PageAdminUsers, // /admin/users
+    }
+}
+```
+
+
+
+A lot of questions at the moment about three configs:
+what if route params AND nested routes
+?-params and hash layer - where is should be configured? looks like this configuration should be moved into page component, not to be in tree.
+so cancel tree idea for now
+
+
+### View Layer (react)
+_Mostly_ template react components without boilerplate, hookafucking, billions of `useEffect` and so. **Styling** moved to **CSS** or... not.
+
+## Tools
+- `createPub` and `qjp` (should be refucktored) from `v01`
+- Some wrapper around `react-router-dom` to pass query func to `Route` component or other configuration
+- **Metaplate**, which provides meta templating abilities to write nesting components page layout, according to nested query data without boilerplate and map
+- Something for **SSR**
+
+## Additionals
+Some addons
+### SSR
+For now no implementations about **SSR**, but there are some _ideas_:
+- CSSR. **Client Server Side Rendering** lol. Draw HTML in user browser, then upload it back to backend. Pros and cons:
+    - Pros:
+        - no need of backend SSR code
+        - no load on server
+    - Cons:
+        - client-side load
+        - client can scam
+        - no SEO HTML before user comes to page
+- **Simplicity**. No backend code with all this **SSR** boilerplate. Using maximal isomorphy as it possible.
+    - **Components** may have **stub** version.
+    - **DOM-Elements** and/or **Components** may be marked as **private** for sensual information. This **DOM-Elements** or **Components** should
+        not be rendered on backend (only stub rendered)
+    - Marking components/elements as **private** gives ability to get seamless **SSR** without ~~almost~~ coding on backend.
+
+
+## TODO
+- **Check speed** or **MetaPlate** on huge updates.
+- **Wrapper** around **react-router-dom**.
+- **SSR** library
+
+

+ 2 - 56
src/App.tsx

@@ -4,72 +4,18 @@ import viteLogo from '/vite.svg'
 import './App.css'
 import {createBrowserHistory} from 'history';
 import {Router} from 'react-router-dom'
-import {NamedRoute, NamedLink, HashRoute, createPrivateRoute} from './lib';
+import {createQueryPub} from './lib/pub';
 
 const history = createBrowserHistory()
 
-const SwapiPeople = ({data:{name, eye_color}}) =>
-<div>
-    <h3 style={{color: eye_color}}>{name}</h3>
-    <HashRoute path="/foo"  component={() => <h1>FOO</h1>} />
-    <HashRoute path="/bar"  component={() => <h1>BAR</h1>} />
-    <a href="#/foo">go to foo</a>
-    <a href="#/bar">go to bar</a>
-</div>
+console.log(createQueryPub)
 
-const SwapiPlanet = ({data:{name, diameter}}) =>
-<div>
-    <h3>{name}</h3>
-    <h4>{diameter}</h4>
-</div>
 
-const queries = {
-    people({id}){
-        return fetch('https://swapi.dev/api/people/' + id).then(res => res.json())
-    },
-    planet({id}){
-        if (id == 2) 
-            return {
-                "name": "Alderaan",
-                "diameter": "12500"
-            }
-        return fetch('https://swapi.dev/api/planets/' + id).then(res => res.json())
-    }
-}
-
-const Pending = () => <h1>Loading</h1>
-const Error   = ({error}) => <h1>Error: {error.message}</h1>
-
-const UserNamedRoute = createPrivateRoute(() => ['user'], history => history.goBack())(NamedRoute)
-
-const Dashboard = () => <h1>dashboard</h1>
 
 
 function App() {
   return (
     <Router history={history}>
-      <div>
-        <NamedRoute name="people" 
-                    query={queries.people}
-                    path="/people/:id" 
-                    pendingQueryComponent={Pending}
-                    component={SwapiPeople}/>
-        <NamedRoute routeName="planet" 
-                    query={queries.planet}
-                    pendingQueryComponent={Pending}
-                    errorQueryComponent={Error}
-                    path="/planet/:id" 
-                    component={SwapiPlanet}/>
-        <UserNamedRoute routeName="dashboard" path="/dashboard" component={Dashboard} roles={['admin']} />
-      </div>
-      <p className="read-the-docs">
-        <NamedLink routeName="people" params={{id: 1}}>Luke</NamedLink>
-        <NamedLink routeName="people" params={{id: 2}}>C3PO</NamedLink>
-      </p>
-      <p className="read-the-docs">
-        <NamedLink routeName="planet" params={{id: 1}}>Tatooin</NamedLink>
-        <NamedLink routeName="planet" params={{id: 2}}>Alderaan</NamedLink>
-      </p>
     </Router>
   )
 }

+ 2 - 0
src/lib/pub/index.ts

@@ -1,6 +1,8 @@
 import createPub from './createPub'
+import createQueryPub from './query'
 
 export type { Pub } from  './createPub'
 export { usePub } from  './react'
 export default createPub;
+export {createQueryPub};
 

+ 59 - 0
src/lib/pub/query/index.ts

@@ -0,0 +1,59 @@
+import createPub from '../';
+
+type QueryFuncType = (...args: Array<any>) => Promise;
+
+type CreateQueryPubOptions = {
+    queryFunc?: QueryFuncType,
+    cacheTimeout?: number,
+    hidePendingOnRefresh?: boolean
+    avoidRepeatInProgress?: boolean,
+}
+
+enum PromiseStatus  {
+    PENDING = "PENDING",
+    FULFILLED = "FULFILLED",
+    REJECTED = "REJECTED"
+}
+
+type PromiseSnapshot = {
+    status: PromiseStatus,
+    payload: any,
+    error: Error
+}
+
+const defaultOptions:CreateQueryPubOptions = {
+    cacheTimeout: 2 * 60 * 1000,
+    hidePendingOnRefresh: true,
+    avoidRepeatInProgress: true,
+}
+
+export default (options:CreateQueryPubOptions) => {
+    const queryPub = createPub({queries: {}, mutations: {}})
+
+    options = {...defaultOptions, ...options}
+
+    const createQuery = (queryFunc: QueryFuncType):Function => {
+        return (promiseName:string, isMutation:boolean) => {
+            const qF = queryFunc || options.queryFunc
+
+            const forceQuery = (...args) => {
+                return qF(...args)
+            }
+
+            const query = (...args) => {
+                if (isMutation && (!options.avoidRepeatInProgress || queryPub.mutations?.[promiseName]?.status !== 'PENDING')){
+                    queryPub.mutations[promiseName] = {status: 'PENDING'}
+                    return forceQuery(...args).then(payload => queryPub.mutations[promiseName] = {status: 'FULFILLED', payload},
+                                                    error   => queryPub.mutations[promiseName] = {status: 'REJECTED', error})
+                }
+                if (!isMutation){
+                    const stringifiedArgs = JSON.stringify(args)
+                    queryPub[promiseName][stringifiedArgs]
+                }
+            }
+            return {forceQuery, query}
+        }
+    }
+
+    return {queryPub, createQuery}
+}

+ 1 - 1
src/lib/router/index.tsx

@@ -116,7 +116,7 @@ export const createPrivateRoute = (getUserRoles:GetUserRolesFunc, defaultFallbac
             return <RouteComponent {...props} component={ComponentWrapper}/>
         }
 
-console.log(createPrivateRoute)
+//console.log(createPrivateRoute)
 
     
 

+ 77 - 0
src/test/route.tsx

@@ -0,0 +1,77 @@
+import { useState, useEffect } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from '/vite.svg'
+import './App.css'
+import {createBrowserHistory} from 'history';
+import {Router} from 'react-router-dom'
+import {NamedRoute, NamedLink, HashRoute, createPrivateRoute} from './lib';
+
+const history = createBrowserHistory()
+
+const SwapiPeople = ({data:{name, eye_color}}) =>
+<div>
+    <h3 style={{color: eye_color}}>{name}</h3>
+    <HashRoute path="/foo"  component={() => <h1>FOO</h1>} />
+    <HashRoute path="/bar"  component={() => <h1>BAR</h1>} />
+    <a href="#/foo">go to foo</a>
+    <a href="#/bar">go to bar</a>
+</div>
+
+const SwapiPlanet = ({data:{name, diameter}}) =>
+<div>
+    <h3>{name}</h3>
+    <h4>{diameter}</h4>
+</div>
+
+const queries = {
+    people({id}){
+        return fetch('https://swapi.dev/api/people/' + id).then(res => res.json())
+    },
+    planet({id}){
+        if (id == 2) 
+            return {
+                "name": "Alderaan",
+                "diameter": "12500"
+            }
+        return fetch('https://swapi.dev/api/planets/' + id).then(res => res.json())
+    }
+}
+
+const Pending = () => <h1>Loading</h1>
+const Error   = ({error}) => <h1>Error: {error.message}</h1>
+
+const UserNamedRoute = createPrivateRoute(() => ['user'], history => history.goBack())(NamedRoute)
+
+const Dashboard = () => <h1>dashboard</h1>
+
+
+function App() {
+  return (
+    <Router history={history}>
+      <div>
+        <NamedRoute name="people" 
+                    query={queries.people}
+                    path="/people/:id" 
+                    pendingQueryComponent={Pending}
+                    component={SwapiPeople}/>
+        <NamedRoute routeName="planet" 
+                    query={queries.planet}
+                    pendingQueryComponent={Pending}
+                    errorQueryComponent={Error}
+                    path="/planet/:id" 
+                    component={SwapiPlanet}/>
+        <UserNamedRoute routeName="dashboard" path="/dashboard" component={Dashboard} roles={['admin']} />
+      </div>
+      <p className="read-the-docs">
+        <NamedLink routeName="people" params={{id: 1}}>Luke</NamedLink>
+        <NamedLink routeName="people" params={{id: 2}}>C3PO</NamedLink>
+      </p>
+      <p className="read-the-docs">
+        <NamedLink routeName="planet" params={{id: 1}}>Tatooin</NamedLink>
+        <NamedLink routeName="planet" params={{id: 2}}>Alderaan</NamedLink>
+      </p>
+    </Router>
+  )
+}
+
+export default App