├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── react-runtime-config-logo.ai └── react-runtime-config-logo.png ├── package.json ├── src ├── createUseAdminConfig.ts ├── createUseConfig.ts ├── index.test.ts ├── index.ts ├── parsers.ts ├── types.ts ├── utils.test.ts └── utils.ts ├── tsconfig.json ├── tsconfig.package.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("eslint").Linter.Config} 3 | */ 4 | module.exports = { 5 | parser: "@typescript-eslint/parser", 6 | 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: "module", 10 | }, 11 | extends: ["plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended"], 12 | rules: { 13 | "@typescript-eslint/no-var-requires": 0, 14 | "@typescript-eslint/explicit-module-boundary-types": 0, 15 | "@typescript-eslint/no-explicit-any": 0, 16 | "@typescript-eslint/no-unused-vars": [ 17 | "error", 18 | { 19 | varsIgnorePattern: "^_", 20 | argsIgnorePattern: "^_", 21 | ignoreRestSiblings: true, 22 | }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/macos,node,vim,visualstudiocode,windows 2 | 3 | ### macOS ### 4 | # General 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | ### Node ### 32 | # Logs 33 | logs 34 | *.log 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # Runtime data 40 | pids 41 | *.pid 42 | *.seed 43 | *.pid.lock 44 | 45 | # Directory for instrumented libs generated by jscoverage/JSCover 46 | lib-cov 47 | 48 | # Coverage directory used by tools like istanbul 49 | coverage 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # Bower dependency directory (https://bower.io/) 58 | bower_components 59 | 60 | # node-waf configuration 61 | .lock-wscript 62 | 63 | # Compiled binary addons (https://nodejs.org/api/addons.html) 64 | build/Release 65 | 66 | # Dependency directories 67 | node_modules/ 68 | jspm_packages/ 69 | 70 | # TypeScript v1 declaration files 71 | typings/ 72 | 73 | # Optional npm cache directory 74 | .npm 75 | 76 | # Optional eslint cache 77 | .eslintcache 78 | 79 | # Optional REPL history 80 | .node_repl_history 81 | 82 | # Output of 'npm pack' 83 | *.tgz 84 | 85 | # Yarn Integrity file 86 | .yarn-integrity 87 | 88 | # dotenv environment variables file 89 | .env 90 | 91 | # parcel-bundler cache (https://parceljs.org/) 92 | .cache 93 | 94 | # next.js build output 95 | .next 96 | 97 | # nuxt.js build output 98 | .nuxt 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless 105 | 106 | ### Vim ### 107 | # Swap 108 | [._]*.s[a-v][a-z] 109 | [._]*.sw[a-p] 110 | [._]s[a-rt-v][a-z] 111 | [._]ss[a-gi-z] 112 | [._]sw[a-p] 113 | 114 | # Session 115 | Session.vim 116 | 117 | # Temporary 118 | .netrwhist 119 | *~ 120 | # Auto-generated tag files 121 | tags 122 | # Persistent undo 123 | [._]*.un~ 124 | 125 | ### VisualStudioCode ### 126 | .vscode/* 127 | !.vscode/settings.json 128 | !.vscode/tasks.json 129 | !.vscode/launch.json 130 | !.vscode/extensions.json 131 | 132 | ### Windows ### 133 | # Windows thumbnail cache files 134 | Thumbs.db 135 | ehthumbs.db 136 | ehthumbs_vista.db 137 | 138 | # Dump file 139 | *.stackdump 140 | 141 | # Folder config file 142 | [Dd]esktop.ini 143 | 144 | # Recycle Bin used on file shares 145 | $RECYCLE.BIN/ 146 | 147 | # Windows Installer files 148 | *.cab 149 | *.msi 150 | *.msix 151 | *.msm 152 | *.msp 153 | 154 | # Windows shortcuts 155 | *.lnk 156 | 157 | 158 | # End of https://www.gitignore.io/api/macos,node,vim,visualstudiocode,windows 159 | 160 | # Build files 161 | dist 162 | lib 163 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 120, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14.16.1" 4 | cache: yarn 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Contiamo GmbH 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 |
2 | react-runtime-config 3 |
4 | 5 |

6 | Make your application easily configurable. 7 |

8 | 9 |

10 | A simple way to provide runtime configuration for your React application, with localStorage overrides and hot-reload support ⚡️! 11 |

12 | 13 |

14 | 15 | npm (tag) 16 | 17 | 18 | travis (tag) 19 | 20 | license MIT (tag) 21 |

22 | 23 | ## Why 24 | 25 | Most web applications usually need to support and function within a variety of distinct environments: local, development, staging, production, on-prem, etc. This project aims to provide flexibility to React applications by making certain properties configurable at runtime, allowing the app to be customized based on a pre-determined configmap respective to the environment. This is especially powerful when combined with [Kubernetes configmaps](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/). 26 | 27 | Here are examples of some real-world values that can be helpful when configurable at runtime: 28 | 29 | - Primary Color 30 | - Backend API URL 31 | - Feature Flags 32 | - … 33 | 34 | ## How 35 | 36 | The configuration can be set by _either_: 37 | 38 | - setting a configuration property on `window` with reasonable defaults. Consider, 39 | 40 | ```js 41 | window.MY_APP_CONFIG = { 42 | primaryColor: "green", 43 | }; 44 | ``` 45 | 46 | - _or_ by setting a value in `localStorage`. Consider, 47 | 48 | ```js 49 | localStorage.setItem("MY_APP_CONFIG.primaryColor", "green"); 50 | ``` 51 | 52 | The `localStorage` option could provide a nice delineation between environments: you _could_ set your local environment to green, and staging to red for example, in order to never be confused about what you're looking at when developing locally and testing against a deployed development environment: if it's green, it's local. 53 | 54 | This configuration is then easily read by the simple React hook that this library exports. 55 | 56 | ## Getting started 57 | 58 | 1. `npm i react-runtime-config` 59 | 1. Create a namespace for your config: 60 | 61 | ```tsx 62 | // components/Config.tsx 63 | import createConfig from "react-runtime-config"; 64 | 65 | /** 66 | * `useConfig` and `useAdminConfig` are now React hooks that you can use in your app. 67 | * 68 | * `useConfig` provides config getter & setter, `useAdminConfig` provides data in order 69 | * to visualize your config map with ease. More on this further down. 70 | */ 71 | export const { useConfig, useAdminConfig } = createConfig({ 72 | namespace: "MY_APP_CONFIG", 73 | schema: { 74 | color: { 75 | type: "string", 76 | enum: ["blue" as const, "green" as const, "pink" as const], // `as const` is required to have nice autocompletion 77 | description: "Main color of the application", 78 | }, 79 | backend: { 80 | type: "string", 81 | description: "Backend url", // config without `default` need to be provided into `window.MY_APP_CONFIG` 82 | }, 83 | port: { 84 | type: "number", // This schema can be retrieved after in `useAdminConfig().fields` 85 | description: "Backend port", 86 | min: 1, 87 | max: 65535, 88 | default: 8000, // config with `default` don't have to be set on `window.MY_APP_CONFIG` 89 | }, 90 | monitoringLink: { 91 | type: "custom", 92 | description: "Link of the monitoring", 93 | parser: value => { 94 | if (typeof value === "object" && typeof value.url === "string" && typeof value.displayName === "string") { 95 | // The type will be inferred from the return type 96 | return { url: value.url as string, displayName: value.displayName as string }; 97 | } 98 | // This error will be shown if the `window.MY_APP_CONFIG.monitoringLink` can't be parsed or if we `setConfig` an invalid value 99 | throw new Error("Monitoring link invalid!"); 100 | }, 101 | }, 102 | isLive: { 103 | type: "boolean", 104 | default: false, 105 | }, 106 | }, 107 | }); 108 | ``` 109 | 110 | You can now use the created hooks everywhere in your application. Thoses hooks are totally typesafe, connected to your configuration. This means that you can easily track down all your configuration usage across your entire application and have autocompletion on the keys. 111 | 112 | ### Usage 113 | 114 | ```tsx 115 | // components/MyComponents.tsx 116 | import react from "React"; 117 | import { useConfig } from "./Config"; 118 | 119 | const MyComponent = () => { 120 | const { getConfig } = useConfig(); 121 | 122 | return

My title

; 123 | }; 124 | ``` 125 | 126 | The title will have a different color regarding our current environment. 127 | 128 | The priority of config values is as follows: 129 | 130 | - `localStorage.getItem("MY_APP_CONFIG.color")` 131 | - `window.MY_APP_CONFIG.color` 132 | - `schema.color.default` 133 | 134 | ## Namespaced `useConfig` hook 135 | 136 | In a large application, you may have multiple instances of `useConfig` from different `createConfig`. So far every `useConfig` will return a set of `getConfig`, `setConfig` and `getAllConfig`. 137 | 138 | To avoid any confusion or having to manually rename every usage of `useConfig` in a large application, you can use the `configNamespace` options. 139 | 140 | ```ts 141 | // themeConfig.ts 142 | export const { useConfig: useThemeConfig } = createConfig({ 143 | namespace: "theme", 144 | schema: {}, 145 | configNamespace: "theme", // <- here 146 | }); 147 | 148 | // apiConfig.ts 149 | export const { useConfig: useApiConfig } = createConfig({ 150 | namespace: "api", 151 | schema: {}, 152 | configNamespace: "api", // <- here 153 | }); 154 | 155 | // App.ts 156 | import { useThemeConfig } from "./themeConfig"; 157 | import { useApiConfig } from "./apiConfig"; 158 | 159 | export const App = () => { 160 | // All methods are now namespaces 161 | // no more name conflicts :) 162 | const { getThemeConfig } = useThemeConfig(); 163 | const { getApiConfig } = useApiConfig(); 164 | 165 | return
; 166 | }; 167 | ``` 168 | 169 | ## Create an Administration Page 170 | 171 | To allow easy management of your configuration, we provide a smart react hook called `useAdminConfig` that provides all the data that you need in order to assemble an awesome administration page where the configuration of your app can be referenced and managed. 172 | 173 | **Note:** we are using [`@operational/components`](https://github.com/contiamo/operational-components) for this example, but a UI of config values _can_ be assembled with any UI library, or even with plain ole HTML-tag JSX. 174 | 175 | ```ts 176 | // pages/ConfigurationPage.tsx 177 | import { Page, Card, Input, Button, Checkbox } from "@operational/components"; 178 | import { useAdminConfig } from "./components/Config"; 179 | 180 | export default () => { 181 | const { fields, reset } = useAdminConfig(); 182 | 183 | return ( 184 | 185 | 186 | {fields.map(field => 187 | field.type === "boolean" ? ( 188 | 189 | ) : ( 190 | 191 | ), 192 | )} 193 | 194 | 195 | 196 | ); 197 | }; 198 | ``` 199 | 200 | You have also access to `field.windowValue` and `field.storageValue` if you want implement more advanced UX on this page. 201 | 202 | ## Multiconfiguration admin page 203 | 204 | As soon as you have more than one configuration in your project, you might want to merge all thoses configurations in one administration page. Of course, you will want a kind of `ConfigSection` component that take the result of any `useAdminConfig()` (so `field`, `reset` and `namespace` as props). 205 | 206 | Spoiler alert, having this kind of component type safe can be tricky, indeed you can try use `ReturnType | ReturnType` as props but typescript will fight you (`Array.map` will tell you that the signature are not compatible). 207 | 208 | Anyway, long story short, this library provide you an easy way to with this: `GenericAdminFields` type. This type is compatible with every configuration and will provide you a nice framework to create an amazing UX. 209 | 210 | ```tsx 211 | import { GenericAdminFields } from "react-runtime-config"; 212 | 213 | export interface ConfigSectionProps { 214 | fields: GenericAdminFields; 215 | namespace: string; 216 | reset: () => void; 217 | } 218 | 219 | export const ConfigSection = ({ namespace, fields }: ConfigSectionProps) => { 220 | return ( 221 |
222 | {fields.map(f => { 223 | if (f.type === "string" && !f.enum) { 224 | return ; 225 | } 226 | if (f.type === "number") { 227 | return ; 228 | } 229 | if (f.type === "boolean") { 230 | return ; 231 | } 232 | if (f.type === "string" && f.enum) { 233 | // `f.set` can take `any` but you still have runtime validation if a wrong value is provided. 234 | return