├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── examples
└── todos
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.css
│ ├── App.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ └── serviceWorker.js
│ └── yarn.lock
├── package.json
├── rollup.config.js
├── src
├── Mirror.ts
├── MirrorActions.tsx
├── MirrorContext.tsx
├── MirrorHandles.tsx
├── MirrorSnapshots.ts
├── NamespacedMirrorImplementation.tsx
├── createMirror.ts
├── index.ts
├── useDocument.ts
└── useSnapshot.ts
├── test
├── MirrorKeyHandle.test.tsx
├── MirrorKeyListHandle.test.tsx
├── createMirror.test.ts
└── useSnapshot.test.tsx
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'react-app',
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .vscode
3 | dist
4 | node_modules
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "jsxBracketSameLine": true,
8 | "parser": "typescript",
9 | "semi": false,
10 | "rcVerbose": false
11 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 James K Nelson
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | 'Software'), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-zen
2 | =========
3 |
4 |
5 |
6 | **A collection of simple utilities for React**
7 |
8 | ```bash
9 | yarn add react-zen
10 | ```
11 |
12 | What?
13 | -----
14 |
15 | Currently, react-zen contains just two utilities:
16 |
17 | - `createMirror(fetcher)`
18 | - `useSnapshot(handle)`
19 |
20 | Together, these two functions let you easily consume asynchronous data in your React components. For example, here's how you'd load and display data from a REST API:
21 |
22 | ```js
23 | import { Suspense } from 'react'
24 | import { createMirror, useSnapshot } from 'react-zen'
25 |
26 | // A mirror automatically fetches data as it is required, and purges it
27 | // once it is no longer in use.
28 | const api = createMirror(async url => {
29 | let response = await fetch(BaseURL+url)
30 | return response.json()
31 | })
32 |
33 | function Screen() {
34 | // useSnapshot returns your data, loading status, etc.
35 | let { data } = useSnapshot(api.key('/todos/1'))
36 | return
{data.title}
37 | }
38 |
39 | function App() {
40 | return (
41 | // Make sure to wrap your any component that use `useSnapshot()`
42 | // with a tag.
43 | Loading}>
44 |
45 |
46 | )
47 | }
48 | ```
49 |
50 | Of course, you'll also want to be able to refresh and update your data. Mirrors and snapshots both have a suite of methods to make this easy. You can see how this works at the live example:
51 |
52 | [See a full featured example at CodeSandbox](https://codesandbox.io/s/broken-water-48o94)
53 |
54 |
55 |
56 | API
57 | ---
58 |
59 | ### `useSnapshot()`
60 |
61 | Returns a snapshot of the data specified by the given handle.
62 |
63 | ```typescript
64 | export function useSnapshot(
65 | handle: MirrorHandle,
66 | ): {
67 | data: Data
68 | key: Key
69 |
70 | /**
71 | * Set to true after `invalidate` has been called, and stays true until a
72 | * more recent version of the document is received.
73 | */
74 | invalidated: boolean
75 |
76 | /**
77 | * Indicates that a fetch is currently taking place.
78 | */
79 | pending: boolean
80 |
81 | /**
82 | * Starts as false, and becomes true once `data` is set for the first time.
83 | */
84 | primed: boolean
85 |
86 | /**
87 | * Marks this key's currently stored snapshot as invalid.
88 | *
89 | * If there's an active subscription, a new version of the data will be
90 | * fetched.
91 | */
92 | invalidate(): void
93 |
94 | /**
95 | * Stores the given data. If there is no subscription for it, then the data
96 | * will be immediately scheduled for purge.
97 | *
98 | * In the case a function is given, if this key has a non-empty snapshot,
99 | * then the updater callback will be called and the returned value will be
100 | * set as the current data. If the key's snapshot is empty or is not yet
101 | * primed, then an error will be thrown.
102 | *
103 | * This will not change the `pending` status of your data.
104 | */
105 | update(dataOrUpdater: Data | MirrorUpdaterCallback): void
106 | }
107 | ```
108 |
109 |
110 | ### `createMirror()`
111 |
112 | Create a mirror of the data in some asynchronous source, where data is automatically fetched as required, and purged when no longer needed.
113 |
114 | ```typescript
115 | createMirror(fetch: (
116 | snapshot: Snapshot,
117 | context: Context
118 | ) => Promise)
119 | ```
120 |
121 |
122 | ### `Mirror`
123 |
124 | The object returned by `createMirror()`.
125 |
126 | ```typescript
127 | interface Mirror {
128 | /**
129 | * Return a handle for the specified key, from which you can get
130 | * and subscribe to its value.
131 | */
132 | key(key: string): MirrorKeyHandle
133 |
134 | /**
135 | * Return a handle for the specified keys, from which you can get
136 | * and subscribe to all their values at once.
137 | */
138 | key(keys: string[]): MirrorKeyListHandle
139 |
140 | /**
141 | * Return a list of the keys currently stored in the mirror for the given
142 | * deps array.
143 | */
144 | knownKeys(): Key[]
145 | }
146 | ```
147 |
148 |
149 | ### `MirrorHandle`
150 |
151 | As returned by `mirror.key(key)` and `mirror.keys(keys)`
152 |
153 | ```typescript
154 | interface MirrorHandle {
155 | key: Key
156 |
157 | /**
158 | * Returns a promise to a mirrored snapshot for this key.
159 | */
160 | get(): Promise>
161 |
162 | /**
163 | * Returns the latest snapshot for the given data.
164 | */
165 | getLatest(): MirrorSnapshot
166 |
167 | /**
168 | * Subscribe to updates to snapshots for the given key. Note that this will
169 | * not immediately emit a snapshot unless subscribing triggers a fetch, and
170 | * adds/updates a snapshot in the process.
171 | */
172 | subscribe(
173 | callback: MirrorSubscribeCallback,
174 | ): MirrorUnsubscribeFunction
175 |
176 | /**
177 | * Marks this key's currently stored snapshot as invalid.
178 | *
179 | * If there's an active subscription, a new version of the data will be
180 | * fetched.
181 | */
182 | invalidate(): void
183 |
184 | /**
185 | * Stores the given data. If there is no subscription for it, then the data
186 | * will be immediately scheduled for purge.
187 | *
188 | * In the case a function is given, if this key has a non-empty snapshot,
189 | * then the updater callback will be called and the returned value will be
190 | * set as the current data. If the key's snapshot is empty or is not yet
191 | * primed, then an error will be thrown.
192 | *
193 | * This will not change the `pending` status of your data.
194 | */
195 | update(dataOrUpdater: Data | MirrorUpdaterCallback): void
196 | }
197 | ```
198 |
199 |
200 | Contributing / Plans
201 | --------------------
202 |
203 | A number of undocumented placeholder functions currently exist, which throw an exception when called. PRs implementing theses would be very welcome. Functions include:
204 |
205 | - `mirror.keys()`, which should allow you to get/subscribe to a list of keys at once.
206 | - `mirror.hydrateFromState()`, which should allow serialized data to be passed from the server to the client.
207 | - `mirror.purge()`, which should allow all data within a mirror to be immediately purged.
208 | - `handle.predictUpdate()`, which should allow for optimistic updates to be recorded against a key.
209 |
210 | A number of *other* features have already been implemented, but need documentation and testing. These include namespaces and `extractState()`, both of which would be useful for server side rendering.
211 |
212 | Once `mirror.keys()` has been implemented, it should be possible to create a `CollectionMirror`, which allows you to subscribe to queries/collections as well as individual records.
213 |
214 | On other feature that would go a long way to improving real world use would configurable strategies for fetching invalidated/initial data -- instead of just giving up after the first fetch, as currently happens. Alongside this, it may make sense to have the ability to command a fetch, as opposed to just marking data as invalidated and allowing fetch to happen automatically.
215 |
216 |
217 | License
218 | -------
219 |
220 | react-zen is MIT licensed.
221 |
--------------------------------------------------------------------------------
/examples/todos/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/todos/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/examples/todos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todos",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react-scripts": "3.2.0",
7 | "react-zen": "link:../.."
8 | },
9 | "scripts": {
10 | "start": "react-scripts start",
11 | "build": "react-scripts build",
12 | "test": "react-scripts test",
13 | "eject": "react-scripts eject"
14 | },
15 | "eslintConfig": {
16 | "extends": "react-app"
17 | },
18 | "browserslist": {
19 | "production": [
20 | ">0.2%",
21 | "not dead",
22 | "not op_mini all"
23 | ],
24 | "development": [
25 | "last 1 chrome version",
26 | "last 1 firefox version",
27 | "last 1 safari version"
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/todos/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesknelson/react-zen/b18d59ece6a63e46bfe18cac27ecd691c2e11794/examples/todos/public/favicon.ico
--------------------------------------------------------------------------------
/examples/todos/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/todos/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesknelson/react-zen/b18d59ece6a63e46bfe18cac27ecd691c2e11794/examples/todos/public/logo192.png
--------------------------------------------------------------------------------
/examples/todos/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesknelson/react-zen/b18d59ece6a63e46bfe18cac27ecd691c2e11794/examples/todos/public/logo512.png
--------------------------------------------------------------------------------
/examples/todos/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/todos/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/examples/todos/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | }
8 |
9 | .App-header {
10 | background-color: #282c34;
11 | min-height: 100vh;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | justify-content: center;
16 | font-size: calc(10px + 2vmin);
17 | color: white;
18 | }
19 |
20 | .App-link {
21 | color: #09d3ac;
22 | }
23 |
24 | input {
25 | width: 50px;
26 | }
--------------------------------------------------------------------------------
/examples/todos/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useState, useEffect } from 'react'
2 | import { createMirror, useSnapshot } from 'react-zen'
3 | import './App.css'
4 |
5 | const BaseURL = 'https://jsonplaceholder.typicode.com'
6 |
7 | // A mirror automatically fetches data as it is required, and purges it
8 | // once it is no longer in use.
9 | const api = createMirror(async url => {
10 | let response = await fetch(BaseURL + url)
11 | return response.json()
12 | })
13 |
14 | function Screen({ id }) {
15 | // useSnapshot returns your data, loading status, etc.
16 | let snapshot = useSnapshot(api.key(`/todos/${id}`))
17 |
18 | const handleToggle = async () => {
19 | let request = await fetch(BaseURL + `/todos/${id}`, {
20 | method: 'PATCH',
21 | body: JSON.stringify({
22 | completed: !snapshot.data.completed,
23 | }),
24 | headers: {
25 | 'Content-type': 'application/json; charset=UTF-8',
26 | },
27 | })
28 | snapshot.update(await request.json())
29 | }
30 |
31 | return (
32 |
40 | )
41 | }
42 |
43 | function App() {
44 | let [knownKeys, setKnownKeys] = useState(api.knownKeys().join(','))
45 | let [id, setId] = useState(1)
46 |
47 | // Keep re-rendering the known keys
48 | useEffect(() => {
49 | let interval = setInterval(() => {
50 | setKnownKeys(api.knownKeys().join(','))
51 | }, 1000)
52 | return () => {
53 | clearInterval(interval)
54 | }
55 | }, [])
56 |
57 | return (
58 |
67 | {/*
68 | Make sure to wrap your any component that use `useSnapshot()`
69 | with a tag.
70 | */}
71 | Loading
}>
72 |
73 |
74 |
cached keys: {knownKeys}
75 |
76 | )
77 | }
78 |
79 | export default App
80 |
--------------------------------------------------------------------------------
/examples/todos/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/examples/todos/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/examples/todos/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/todos/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-zen",
3 | "version": "0.2.0",
4 | "description": "A collection of simple hooks, components and functions for React",
5 | "author": "James K Nelson ",
6 | "license": "MIT",
7 | "main": "dist/commonjs/index.js",
8 | "module": "dist/es/index.js",
9 | "types": "dist/types/index.d.ts",
10 | "scripts": {
11 | "clean": "rimraf dist",
12 | "build:commonjs": "tsc --pretty --module commonjs --outDir dist/commonjs",
13 | "build:es": "tsc --pretty --module es2015 --outDir dist/es",
14 | "build:types": "tsc --pretty --declaration --emitDeclarationOnly --outDir dist/types --isolatedModules false",
15 | "build:umd": "tsc --pretty --declaration --module es2015 --outDir dist/umd-intermediate && cross-env NODE_ENV=development rollup -c -o dist/umd/navi.js && rimraf dist/umd-intermediate",
16 | "build:umd:min": "tsc --pretty --declaration --module es2015 --outDir dist/umd-intermediate && cross-env NODE_ENV=production rollup -c -o dist/umd/navi.min.js && rimraf dist/umd-intermediate",
17 | "build": "yarn run clean && yarn build:es && yarn build:commonjs && yarn build:types && yarn build:umd && yarn build:umd:min",
18 | "build:watch": "yarn run clean && yarn build:es -- --types --watch",
19 | "prepare": "yarn test && yarn build",
20 | "test": "jest",
21 | "test:watch": "jest --watch"
22 | },
23 | "devDependencies": {
24 | "@types/jest": "^24.0.19",
25 | "@types/node": "^12.11.7",
26 | "@types/react": "^16.9.11",
27 | "@types/react-dom": "^16.9.3",
28 | "cross-env": "^6.0.3",
29 | "eslint-config-react-app": "^5.0.2",
30 | "jest": "^24.9.0",
31 | "react": "^16.11.0",
32 | "react-dom": "^16.11.0",
33 | "react-test-renderer": "^16.11.0",
34 | "rimraf": "^3.0.0",
35 | "rollup": "^1.25.2",
36 | "rollup-plugin-commonjs": "^10.1.0",
37 | "rollup-plugin-node-resolve": "^5.2.0",
38 | "rollup-plugin-replace": "^2.2.0",
39 | "rollup-plugin-terser": "^5.1.2",
40 | "ts-jest": "^24.1.0",
41 | "typescript": "^3.6.4"
42 | },
43 | "peerDependencies": {
44 | "react": "^16.11.0"
45 | },
46 | "files": [
47 | "dist"
48 | ],
49 | "keywords": [
50 | "react",
51 | "zen",
52 | "data"
53 | ],
54 | "jest": {
55 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
56 | "testEnvironment": "jsdom",
57 | "moduleFileExtensions": [
58 | "js",
59 | "json",
60 | "jsx",
61 | "ts",
62 | "tsx"
63 | ],
64 | "preset": "ts-jest",
65 | "testMatch": null,
66 | "globals": {
67 | "ts-jest": {
68 | "babelConfig": null,
69 | "diagnostics": false
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is based on the rollup config from Redux
3 | * Copyright (c) 2015-present Dan Abramov
4 | */
5 |
6 | import nodeResolve from 'rollup-plugin-node-resolve'
7 | import commonjs from 'rollup-plugin-commonjs'
8 | import replace from 'rollup-plugin-replace'
9 | import { terser } from 'rollup-plugin-terser'
10 |
11 | const env = process.env.NODE_ENV
12 | const config = {
13 | external: ['react'],
14 | input: 'dist/umd-intermediate/index.js',
15 | output: {
16 | format: 'umd',
17 | globals: {
18 | react: 'React',
19 | },
20 | name: 'ReactZenData',
21 | },
22 | onwarn: function(warning) {
23 | // Suppress warning caused by TypeScript classes using "this"
24 | // https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined
25 | if (warning.code === 'THIS_IS_UNDEFINED') {
26 | return
27 | }
28 | console.error(warning.message)
29 | },
30 | plugins: [
31 | nodeResolve({
32 | jsnext: true,
33 | main: true,
34 | }),
35 |
36 | commonjs(),
37 |
38 | replace({
39 | 'process.env.NODE_ENV': JSON.stringify(env),
40 | }),
41 | ],
42 | }
43 |
44 | if (env === 'production') {
45 | config.plugins.push(terser())
46 | }
47 |
48 | export default config
49 |
--------------------------------------------------------------------------------
/src/Mirror.ts:
--------------------------------------------------------------------------------
1 | import { MirrorDocumentHandle, MirrorDocumentListHandle } from './MirrorHandles'
2 | import { MirrorDocumentSnapshot } from './MirrorSnapshots'
3 |
4 | export interface Mirror
5 | extends NamespacedMirror {
6 | /**
7 | * Returns a namespace to hold data associated with the specified context.
8 | */
9 | namespace(context: Context): NamespacedMirror
10 | }
11 |
12 | export interface NamespacedMirror {
13 | context: Context
14 |
15 | /**
16 | * Allows the current state for a given set of context to be extracted
17 | * so that it can be sent with a server-rendered page, and then
18 | * hydrated on the client.
19 | */
20 | extractState(): any
21 |
22 | /**
23 | * Call this with the state returned from `serializeState()` to hydrate the
24 | * store with data fetched on the server.
25 | */
26 | hydrateFromState(extractedState: any): void
27 |
28 | /**
29 | * Return a handle for the specified key, from which you can get
30 | * and subscribe to its value.
31 | */
32 | key(key: Key): MirrorDocumentHandle
33 |
34 | /**
35 | * Return a handle for an array of keys, from which you can get
36 | * and subscribe to multiple values at once.
37 | */
38 | keys(keys: Key[]): MirrorDocumentListHandle
39 |
40 | /**
41 | * Return a list of the keys currently stored in the mirror for the given
42 | * deps array.
43 | */
44 | knownKeys(): Key[]
45 |
46 | /**
47 | * Immediately purges all cached data for this context.
48 | *
49 | * Note that even if `purge` is *not* called, data will usually be cleaned up
50 | * after it no longer has any subscriptions -- depending on how schedulePurge
51 | * is configured.
52 | *
53 | * This is useful for server-side rendering, where once the request is done,
54 | * you probably don't want any of the data to stick around.
55 | */
56 | purge(): void
57 | }
58 |
59 | export interface MirrorOptions {
60 | /**
61 | * An optional function for computing string keys from mirror keys,
62 | * which is required as documents are stored with string keys internally.
63 | *
64 | * By default, this uses JSON.stringify()
65 | */
66 | computeHashForKey: (key: Key) => string
67 |
68 | /**
69 | * If provided, this will be called whenever the latest snapshot changes.
70 | * Then, if a function is returned, it will be called after the snapshot
71 | * changes again, or after the data is purged.
72 | *
73 | * Use this function to perform side effects based on the currently stored
74 | * data.
75 | *
76 | * For example, if this mirror contains lists of keys, you could create an
77 | * effect to hold those keys within another mirror. Similarly, if this
78 | * mirror contains items that are indexed in another mirror, you coudl use
79 | * an effect to invalidate indexes as the indexed items change.
80 | */
81 | effect?: MirrorEffectFunction
82 |
83 | /**
84 | * This function is called by the mirror to fetch data once a subscription
85 | * has been made to the specified key.
86 | *
87 | * At minimum, the function should return the data associated with the
88 | * specified key, or `null` if there is no data associated with the given
89 | * key.
90 | *
91 | * If nested data is also available in the response, it can be stored by
92 | * calling `store` or `update` on this or other mirror namespaces.
93 | */
94 | fetch?: MirrorFetchFunction
95 |
96 | // If supplied, this will be used instead of fetch, and can fetch multiple
97 | // keys in a single call.
98 | fetchMany?: MirrorFetchManyFunction
99 |
100 | /**
101 | * Configures how to purge data when there are no longer any active
102 | * subscriptions.
103 | *
104 | * If a number is given, data will be purged that many milliseconds after
105 | * it is no longer required.
106 | *
107 | * If a function is given, it'll be called with a purge function that should
108 | * be called once the data should be purged. This function should also
109 | * return a function which can be called to *cancel* a purge, should the
110 | * data become required before the purge takes place.
111 | */
112 | schedulePurge: number | MirrorPurgeScheduler
113 | }
114 |
115 | export type MirrorCancelScheduledPurgeFunction = () => void
116 |
117 | export type MirrorCleanupEffectFunction = () => void
118 |
119 | export type MirrorEffectFunction = (
120 | snapshot: MirrorDocumentSnapshot,
121 | context: Context,
122 | ) => MirrorCleanupEffectFunction
123 |
124 | export type MirrorFetchFunction = (
125 | key: Key,
126 | context: Context,
127 | mirror: NamespacedMirror,
128 | ) => Promise
129 |
130 | export type MirrorFetchManyFunction = (
131 | key: Key[],
132 | context: Context,
133 | mirror: NamespacedMirror,
134 | ) => Promise
135 |
136 | export type MirrorPurgeScheduler = (
137 | purge: () => void,
138 | snapshot: MirrorDocumentSnapshot,
139 | context: Context,
140 | ) => MirrorCancelScheduledPurgeFunction
141 |
--------------------------------------------------------------------------------
/src/MirrorActions.tsx:
--------------------------------------------------------------------------------
1 | export type MirrorCancelHoldFunction = () => void
2 | export type MirrorUpdaterCallback = (
3 | data: UpdateData,
4 | key: Key,
5 | ) => UpdateData
6 |
7 | export interface MirrorActions {
8 | /**
9 | * Instructs the mirror to keep any snapshots of this key, even when there
10 | * is no active subscription.
11 | *
12 | * Note that holding a key will not actively fetch its contents.
13 | */
14 | hold(): MirrorCancelHoldFunction
15 |
16 | /**
17 | * Marks this key's currently stored snapshot as invalid.
18 | *
19 | * If there's an active subscription, a new version of the data will be
20 | * fetched if possible.
21 | */
22 | invalidate(): void
23 |
24 | /**
25 | * Indicate that you expect the snapshot for this key to change in the near
26 | * future.
27 | *
28 | * This prevents any fetches from taking place, sets the `pending`
29 | * indicator to `true`, and if an updater is provided, sets a temporary
30 | * value for `data` from until the returned promise resolves. If the
31 | * returned promise is rejected, the reason will be added to `failure`.
32 | */
33 | predictUpdate(
34 | dataOrUpdater?: UpdateData | MirrorUpdaterCallback,
35 | ): Promise
36 |
37 | /**
38 | * Stores the given data. If there is no subscription for it, then the data
39 | * will be immediately scheduled for purge.
40 | *
41 | * In the case a function is given, if this key has a non-empty snapshot,
42 | * then the updater callback will be called and the returned value will be
43 | * set as the current data. If the key's snapshot is empty or is not yet
44 | * primed, then an error will be thrown.
45 | *
46 | * This will not change the `pending` status of your data.
47 | */
48 | update(
49 | dataOrUpdater: UpdateData | MirrorUpdaterCallback,
50 | ): void
51 | }
52 |
--------------------------------------------------------------------------------
/src/MirrorContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import { Mirror } from './Mirror'
3 |
4 | export interface MirrorContext {
5 | namespace: any
6 | namespaces: Map
7 | }
8 |
9 | export const MirrorContext = React.createContext({
10 | namespace: null,
11 | namespaces: new Map(),
12 | })
13 |
14 | export function MirrorProvider({ children, namespace, namespaces }) {
15 | let context = useMemo(() => ({ namespace, namespaces }), [
16 | namespace,
17 | namespaces,
18 | ])
19 |
20 | return
21 | }
22 |
--------------------------------------------------------------------------------
/src/MirrorHandles.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | MirrorDocumentSnapshot,
3 | MirrorPrimedDocumentSnapshot,
4 | MirrorSnapshot,
5 | MirrorPrimedSnapshot,
6 | } from './MirrorSnapshots'
7 | import { MirrorActions } from './MirrorActions'
8 |
9 | export type MirrorCancelHoldFunction = () => void
10 | export type MirrorSubscribeCallback