Provide a typesafe runtime configuration inside a react app
npm install react-runtime-config
A simple way to provide runtime configuration for your React application, with localStorage overrides and hot-reload support ⚡️!
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.
Here are examples of some real-world values that can be helpful when configurable at runtime:
- Primary Color
- Backend API URL
- Feature Flags
- …
The configuration can be set by _either_:
- setting a configuration property on window with reasonable defaults. Consider,
``js`
window.MY_APP_CONFIG = {
primaryColor: "green",
};
- _or_ by setting a value in localStorage. Consider,
`js`
localStorage.setItem("MY_APP_CONFIG.primaryColor", "green");
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.
This configuration is then easily read by the simple React hook that this library exports.
1. npm i react-runtime-config
1. Create a namespace for your config:
`tsx
// components/Config.tsx
import createConfig from "react-runtime-config";
/**
* useConfig and useAdminConfig are now React hooks that you can use in your app.useConfig
*
* provides config getter & setter, useAdminConfig provides data in orderas const
* to visualize your config map with ease. More on this further down.
*/
export const { useConfig, useAdminConfig } = createConfig({
namespace: "MY_APP_CONFIG",
schema: {
color: {
type: "string",
enum: ["blue" as const, "green" as const, "pink" as const], // is required to have nice autocompletiondefault
description: "Main color of the application",
},
backend: {
type: "string",
description: "Backend url", // config without need to be provided into window.MY_APP_CONFIGuseAdminConfig().fields
},
port: {
type: "number", // This schema can be retrieved after in default
description: "Backend port",
min: 1,
max: 65535,
default: 8000, // config with don't have to be set on window.MY_APP_CONFIGwindow.MY_APP_CONFIG.monitoringLink
},
monitoringLink: {
type: "custom",
description: "Link of the monitoring",
parser: value => {
if (typeof value === "object" && typeof value.url === "string" && typeof value.displayName === "string") {
// The type will be inferred from the return type
return { url: value.url as string, displayName: value.displayName as string };
}
// This error will be shown if the can't be parsed or if we setConfig an invalid value`
throw new Error("Monitoring link invalid!");
},
},
isLive: {
type: "boolean",
default: false,
},
},
});
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.
`tsx
// components/MyComponents.tsx
import react from "React";
import { useConfig } from "./Config";
const MyComponent = () => {
const { getConfig } = useConfig();
return
The title will have a different color regarding our current environment.
The priority of config values is as follows:
-
localStorage.getItem("MY_APP_CONFIG.color")
- window.MY_APP_CONFIG.color
- schema.color.defaultNamespaced
useConfig hookIn 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.To avoid any confusion or having to manually rename every usage of
useConfig in a large application, you can use the configNamespace options.`ts
// themeConfig.ts
export const { useConfig: useThemeConfig } = createConfig({
namespace: "theme",
schema: {},
configNamespace: "theme", // <- here
});// apiConfig.ts
export const { useConfig: useApiConfig } = createConfig({
namespace: "api",
schema: {},
configNamespace: "api", // <- here
});
// App.ts
import { useThemeConfig } from "./themeConfig";
import { useApiConfig } from "./apiConfig";
export const App = () => {
// All methods are now namespaces
// no more name conflicts :)
const { getThemeConfig } = useThemeConfig();
const { getApiConfig } = useApiConfig();
return
;
};
`Create an Administration Page
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.@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.`ts
// pages/ConfigurationPage.tsx
import { Page, Card, Input, Button, Checkbox } from "@operational/components";
import { useAdminConfig } from "./components/Config";export default () => {
const { fields, reset } = useAdminConfig();
return (
{fields.map(field =>
field.type === "boolean" ? (
) : (
),
)}
);
};
`You have also access to
field.windowValue and field.storageValue if you want implement more advanced UX on this page.Multiconfiguration admin page
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).Spoiler alert, having this kind of component type safe can be tricky, indeed you can try use
ReturnType as props but typescript will fight you (Array.map will tell you that the signature are not compatible).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.`tsx
import { GenericAdminFields } from "react-runtime-config";export interface ConfigSectionProps {
fields: GenericAdminFields;
namespace: string;
reset: () => void;
}
export const ConfigSection = ({ namespace, fields }: ConfigSectionProps) => {
return (
{fields.map(f => {
if (f.type === "string" && !f.enum) {
return ;
}
if (f.type === "number") {
return ;
}
if (f.type === "boolean") {
return ;
}
if (f.type === "string" && f.enum) {
//
f.set can take any but you still have runtime validation if a wrong value is provided.
return ;
}
if (f.type === "custom") {
/ Add some special handler/typeguard to retrieve the safety /
}
})}
);
};
`PS: If you have a better idea/pattern, please open an issue to tell me about it 😃
Moar Power (if needed)
We also expose from
createConfig a simple getConfig, getAllConfig and setConfig. These functions can be used standalone and do not require use of the useConfig react hooks. This can be useful for accessing or mutating configuration values in component lifecycle hooks, or anywhere else outside of render.These functions are exactly the same as their counterparts available inside the
useConfig` react hook, the only thing you lose is the hot config reload.