├── .editorconfig
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── labels.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── package.json
├── src
├── consumer.tsx
├── context.ts
├── helpers.ts
├── index.ts
├── provider.tsx
└── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Note: prettier inherits from `indent_style`, `indent_size`/`tab_width`, and `max_line_length`
2 | # https://github.com/prettier/prettier/blob/cecf0657a521fa265b713274ed67ca39be4142cf/docs/api.md#prettierresolveconfigfilepath--options
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [.vscode/settings.json]
13 | indent_size = 4
14 |
15 | # Git is sensitive to whitespace in diff files
16 | # https://stackoverflow.com/questions/50258565/git-editing-hunks-fails-when-file-has-other-hunks/50275053#50275053
17 | [*.diff]
18 | trim_trailing_whitespace = false
19 |
20 | [*.{ts,tsx}]
21 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length
22 | max_line_length = 100
23 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These users will be the default owners for everything in the repo.
2 | # See https://github.com/blog/2392-introducing-code-owners
3 |
4 | * @unsplash/web
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### Steps to Reproduce
2 |
3 | -
4 |
5 | #### Observed Behaviour
6 |
7 | -
8 |
9 | > _Image or video please._
10 |
11 | #### Expected Behaviour
12 |
13 | -
14 |
15 | #### Technical Notes
16 |
17 | -
18 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### Overview
2 |
3 | -
4 |
5 | - Closes #
6 |
7 | #### Todo
8 |
9 | -
10 |
--------------------------------------------------------------------------------
/.github/labels.json:
--------------------------------------------------------------------------------
1 | [
2 | { "name": "Priority: Critical", "color": "#e11d21" },
3 |
4 | { "name": "Status: Blocked", "color": "#e11d21" },
5 | { "name": "Status: On Hold", "color": "#e11d21" },
6 | { "name": "Status: In Progress", "color": "#fbca04" },
7 | { "name": "Status: Ready for Review", "color": "#bfe5bf" },
8 |
9 | { "name": "Type: Bug", "color": "#e11d21" },
10 | { "name": "Type: Maintenance", "color": "#fbca04" },
11 | { "name": "Type: Enhancement", "color": "#84b6eb" }
12 | ]
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Dependency directory
5 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
6 | node_modules
7 |
8 | # Yarn
9 | yarn-error.log
10 |
11 | # typescript dist
12 | dist/
13 |
14 | # VSCode
15 | .vscode/
16 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | tests/ReactSixteenAdapter.js
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Unsplash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-progressive-enhancement
2 | _A handy collection of HOCs for universally rendered apps 🤩_
3 |
4 |
5 | [](https://travis-ci.com/unsplash/react-progressive-enhancement)
6 |
7 | TL;DR In universally rendered React apps, it is common to branch data-fetching and component-rendering depending on the environment (server or client), and defer rendering (a.k.a. "progressively enhance") some components. However, we must ensure the first client render matches the server render.
8 |
9 | This module achieves all the above by tracking whether or not the render mode is "enhanced" with an `isEnhanced` boolean (`true` only after first client render, otherwise `false`), which is accessed through a `withIsEnhanced` HOC. Additionally a `progressivelyEnhanced` HOC is provided which only renders the composed component for enhanced renders.
10 |
11 | For more info, check out [this blog post](https://medium.com/unsplash/react-progressive-enhancement-a-handy-collection-of-hocs-for-universally-rendered-apps-904f689768cf).
12 |
13 | ## Features
14 |
15 | * No dependencies (other than React ^16.3)
16 | * Written in TypeScript (type-annotated)
17 | * Easily extensible through the exported React Context's `Consumer` & `Provider`
18 |
19 | ## Install
20 |
21 | ```bash
22 | yarn add react-progressive-enhancement
23 | # OR
24 | npm install react-progressive-enhancement
25 | ```
26 |
27 | ## Usage
28 |
29 | * Root.jsx:
30 |
31 | ```jsx
32 | import { enableProgressiveEnhancementsOnMount } from 'react-progressive-enhancement';
33 |
34 | const Root = () => (
35 |
38 | );
39 |
40 | export default enableProgressiveEnhancementsOnMount(Root);
41 | ```
42 |
43 |
44 | * PhotoRoute.jsx:
45 |
46 | ```jsx
47 | import { withIsEnhanced, progressivelyEnhance } from 'react-progressive-enhancement';
48 |
49 | const ProgressivelyEnhancedRelatedContent = progressivelyEnhance(RelatedContent);
50 |
51 | class PhotoRoute extends React.Component {
52 | componentDidMount() {
53 | const hasDataFromServer = !this.props.isEnhanced;
54 |
55 | if (!hasDataFromServer) {
56 | this.getPhotoRouteData();
57 | } else {
58 | // do nothing, because the server already fetched the data and passed it to the client.
59 | }
60 | }
61 |
62 | render() {
63 | return (
64 |
65 |
66 | {/* This component will only render after the first client render */}
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | export default withIsEnhanced(PhotoRoute);
74 | ```
75 |
76 | ## API Reference
77 |
78 | #### enableProgressiveEnhancementsOnMount
79 | ```tsx
80 | (ComposedComponent: React.Component) => React.Component
81 | ```
82 |
83 | An HOC that wraps `ComposedComponent` with the Context Provider. `ComposedComponent` should be the root-most Component in your React app.
84 |
85 | #### withIsEnhanced
86 | ```tsx
87 | (ComposedComponent: React.Component) => React.Component
88 | ```
89 |
90 | An HOC that provides the `isEnhanced` prop to `ComposedComponent`.
91 |
92 | #### progressivelyEnhance
93 | ```tsx
94 | (ComposedComponent: React.Component) => React.Component
95 | ```
96 |
97 | An HOC that defers rendering `ComposedComponent` until after the first client render.
98 |
99 | #### Consumer, Provider
100 | ```tsx
101 | React.Context
102 | ```
103 |
104 | The Context's `Consumer` and `Provider` are exported as well, so that you can easily extend this library as you see fit.
105 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-progressive-enhancement",
3 | "version": "0.0.10",
4 | "description": "React Context that progressively enhances components",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist/",
9 | "yarn.lock"
10 | ],
11 | "dependencies": {
12 | "@types/react": "^18.0.8"
13 | },
14 | "devDependencies": {
15 | "@types/react-dom": "^18.0.0",
16 | "react": "^18.1.0",
17 | "react-dom": "^18.1.0",
18 | "ts-node": "^6.1.0",
19 | "typescript": "^4.6.3"
20 | },
21 | "peerDependencies": {
22 | "react": "^17.0.1 || ^18.0.0"
23 | },
24 | "scripts": {
25 | "compile": "tsc --project src/tsconfig.json",
26 | "prepublishOnly": "npm run compile"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/unsplash/react-progressive-enhancement.git"
31 | },
32 | "contributors": [
33 | {
34 | "name": "Sami Jaber",
35 | "email": "jabersami@gmail.com"
36 | }
37 | ],
38 | "engines": {
39 | "yarn": "^1.5.1"
40 | },
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/unsplash/react-progressive-enhancement/issues"
44 | },
45 | "homepage": "https://github.com/unsplash/react-progressive-enhancement#readme"
46 | }
47 |
--------------------------------------------------------------------------------
/src/consumer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { context, ProgressiveEnhancementProp } from './context';
4 | import { ObjectOmit, getDisplayName } from './helpers';
5 |
6 | const { Consumer } = context;
7 |
8 | export const withIsEnhanced = (
9 | ComposedComponent: React.ComponentType,
10 | ) => {
11 | type ComponentWithIsEnhancedType = React.FC>;
12 | const ComponentWithIsEnhanced: ComponentWithIsEnhancedType = (props) => (
13 |
14 | {({ isEnhanced }) => (
15 |
24 | )}
25 |
26 | );
27 |
28 | ComponentWithIsEnhanced.displayName = `withIsEnhanced(${getDisplayName(ComposedComponent)})`;
29 |
30 | return ComponentWithIsEnhanced;
31 | };
32 |
33 | export const progressivelyEnhance = (
34 | ComposedComponent: React.ComponentType,
35 | ) => {
36 | const ProgressivelyEnhance: React.FC = (props) => (
37 |
38 | {({ isEnhanced }) => (isEnhanced ? : null)}
39 |
40 | );
41 |
42 | ProgressivelyEnhance.displayName = `ProgressivelyEnhance(${getDisplayName(ComposedComponent)})`;
43 |
44 | return ProgressivelyEnhance;
45 | };
46 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type ProgressiveEnhancementProp = {
4 | isEnhanced: boolean;
5 | };
6 |
7 | export const defaultValue: ProgressiveEnhancementProp = {
8 | isEnhanced: false,
9 | };
10 |
11 | export const context = React.createContext(defaultValue);
12 |
13 | context.displayName = 'ProgressiveEnhancementContext';
14 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from 'react';
2 |
3 | export const getDisplayName = (ComposedComponent: ComponentType) =>
4 | ComposedComponent.displayName !== undefined ? ComposedComponent.displayName : 'Component';
5 |
6 | export type Omit = Pick>;
7 | export type ObjectOmit = Omit;
8 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { progressivelyEnhance, withIsEnhanced } from './consumer';
2 | export { enableProgressiveEnhancementsOnMount } from './provider';
3 | export { context, ProgressiveEnhancementProp } from './context';
4 |
--------------------------------------------------------------------------------
/src/provider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ProgressiveEnhancementProp, context, defaultValue } from './context';
4 | import { getDisplayName } from './helpers';
5 |
6 | const { Provider } = context;
7 |
8 | export const enableProgressiveEnhancementsOnMount = (
9 | ComposedComponent: React.ComponentType,
10 | ): React.ComponentType => {
11 | class ProgressiveEnhancementProvider extends React.Component {
12 | static displayName = `ProgressiveEnhancementProvider(${getDisplayName(ComposedComponent)})`;
13 |
14 | state = defaultValue;
15 |
16 | componentDidMount() {
17 | this.setState({ isEnhanced: true });
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | return ProgressiveEnhancementProvider;
30 | };
31 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "lib": [
6 | // start defaults
7 | "dom",
8 | // end defaults
9 | "es2015"
10 | ],
11 | "jsx": "react",
12 | "declaration": true,
13 | "outDir": "../dist",
14 | "strict": true
15 | },
16 | "files": ["./index.ts"]
17 | }
18 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/prop-types@*":
6 | version "15.7.1"
7 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6"
8 | integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==
9 |
10 | "@types/react-dom@^18.0.0":
11 | version "18.0.0"
12 | resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141"
13 | integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==
14 | dependencies:
15 | "@types/react" "*"
16 |
17 | "@types/react@*", "@types/react@^18.0.8":
18 | version "18.0.8"
19 | resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.8.tgz#a051eb380a9fbcaa404550543c58e1cf5ce4ab87"
20 | integrity sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==
21 | dependencies:
22 | "@types/prop-types" "*"
23 | "@types/scheduler" "*"
24 | csstype "^3.0.2"
25 |
26 | "@types/scheduler@*":
27 | version "0.16.2"
28 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
29 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
30 |
31 | arrify@^1.0.0:
32 | version "1.0.1"
33 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
34 |
35 | buffer-from@^1.0.0:
36 | version "1.1.0"
37 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
38 |
39 | csstype@^3.0.2:
40 | version "3.0.6"
41 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef"
42 | integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==
43 |
44 | diff@^3.1.0:
45 | version "3.5.0"
46 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
47 |
48 | "js-tokens@^3.0.0 || ^4.0.0":
49 | version "4.0.0"
50 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
51 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
52 |
53 | loose-envify@^1.1.0:
54 | version "1.4.0"
55 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
56 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
57 | dependencies:
58 | js-tokens "^3.0.0 || ^4.0.0"
59 |
60 | make-error@^1.1.1:
61 | version "1.3.4"
62 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.4.tgz#19978ed575f9e9545d2ff8c13e33b5d18a67d535"
63 |
64 | minimist@0.0.8:
65 | version "0.0.8"
66 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
67 |
68 | minimist@^1.2.0:
69 | version "1.2.0"
70 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
71 |
72 | mkdirp@^0.5.1:
73 | version "0.5.1"
74 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
75 | dependencies:
76 | minimist "0.0.8"
77 |
78 | react-dom@^18.1.0:
79 | version "18.1.0"
80 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
81 | integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
82 | dependencies:
83 | loose-envify "^1.1.0"
84 | scheduler "^0.22.0"
85 |
86 | react@^18.1.0:
87 | version "18.1.0"
88 | resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
89 | integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
90 | dependencies:
91 | loose-envify "^1.1.0"
92 |
93 | scheduler@^0.22.0:
94 | version "0.22.0"
95 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
96 | integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
97 | dependencies:
98 | loose-envify "^1.1.0"
99 |
100 | source-map-support@^0.5.6:
101 | version "0.5.6"
102 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13"
103 | dependencies:
104 | buffer-from "^1.0.0"
105 | source-map "^0.6.0"
106 |
107 | source-map@^0.6.0:
108 | version "0.6.1"
109 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
110 |
111 | ts-node@^6.1.0:
112 | version "6.1.0"
113 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-6.1.0.tgz#a2c37a11fdb58e60eca887a1269b025cf4d2f8b8"
114 | dependencies:
115 | arrify "^1.0.0"
116 | diff "^3.1.0"
117 | make-error "^1.1.1"
118 | minimist "^1.2.0"
119 | mkdirp "^0.5.1"
120 | source-map-support "^0.5.6"
121 | yn "^2.0.0"
122 |
123 | typescript@^4.6.3:
124 | version "4.6.3"
125 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
126 | integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
127 |
128 | yn@^2.0.0:
129 | version "2.0.0"
130 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
131 |
--------------------------------------------------------------------------------