@psenger/react-static-config-loader

React Static Config Loader, a convenience tag providing a widely used pattern of loading a static configuration from a server and using ReactJS Context, providing the cofnig to the context of components within the hiearchy in a clean and consistent manner

Usage no npm install needed!

<script type="module">
  import psengerReactStaticConfigLoader from 'https://cdn.skypack.dev/@psenger/react-static-config-loader';
</script>

README

react-static-config-loader

React Static Config Loader, a convenience component providing a widely used pattern of loading a static configuration from a server and injecting the configuration into the ReactJS Context, providing the config within the hierarchy in a clean and consistent manner.

Ideally, the best possible solution would be to bundle a configuration with the build. However, for many, this is not practical.

The react-static-config-loader component, utilizes ReactJS Context, and injects a value ( a configuration ) into React Classes. Unfortunately ( as of React 16 ) is incapable of inject it into JSX functions ( which make sense as JSX are intended to be "Pure" ). This component does provide prebuilt object that can overcome this by creating HOC's and injecting the context values into the props.

Table of contents

Install

This project, hosted alternatively in GitHub, not NPM, requires you append the following to a project level file ./.npmrc

@psenger:registry=https://npm.pkg.github.com

Once completed, you can then execute either npm or yarn to install.

npm install @psenger/react-static-config-loader --save

Usage

In the simplest example, we want to simply fetch a configuration json file from the server and send the config into the ReactJS context of the children. The async function ( fn in this example ) is passed as a property called loader.

While loading ( and default behaviour ), any JSX passed to loadingMsg will be called.


import React from 'react';
import { StaticConfigWrapper, Context } from 'react-static-config-loader';

export class ExampleClass extends React.Component {
  static contextType = Context;
  render() {
    const {someValue} = this.props;
    const config = this.context;
    return <React.Fragment>
      <code>{JSON.stringify(config,null,4)}</code>
      <div>{someValue}</div>
    </React.Fragment>
  }
}

// refer to `later` in the reference section

const App = () => {
  const fn = ()=> Promise.resolve({msg:'go',version:1234,selection:['no','yes'], buttonName:'go go button'})
  return (
    <React.Fragment>
      <StaticConfigWrapper loader={async () => later(2000, fn)}>
        <ExampleClass someValue={'You made it in ExampleClass'}/>
      </StaticConfigWrapper>
    </React.Fragment>
  )
}

export default App

Things get a little complicated if you have "Pure" JSX functions. In this case, the contextType is simply not available. You can bypass this by creating a Higher Order Component (HOC) and pass the value down via the properties or use the built in ConfigPropExtenderHoc which extends the component and copies the config into the component as properties.

import React from "react";
import { ConfigPropExtenderHoc, StaticConfigWrapper } from "@psenger/react-static-config-loader";

const PureFunction = ({ config, someValue }) => <React.Fragment>
  <code>{JSON.stringify(config, null, 4)}</code>
  <div>{someValue}</div>
</React.Fragment>

const HOC = ({someValue}) => {
  return (
    <ConfigPropExtenderHoc>
      <PureFunction someValue={someValue} />
    </ConfigPropExtenderHoc>
  );
}

// refer to `later` in the reference section

const App = () => {
  const fn = ()=> Promise.resolve({msg:'go',version:1234,selection:['no','yes'], buttonName:'go go button'})
  return (
    <React.Fragment>
      <StaticConfigWrapper loader={async () => later(2000, fn)} loadingMsg={<div>Loading</div>}>
        <HOC someValue={'You made it in ExampleClass'}/>
      </StaticConfigWrapper>
    </React.Fragment>
  )
}

export default App

Reference JavaScript

const later = (delay, fnLater) => Promise.resolve()
  .then(()=>{
    let id;
    return new Promise(function(resolve) {
      if (id) { // this is PURELY a safety precaution
        clearTimeout(id);
        id = undefined;
      }
      id = setTimeout(resolve, delay);
    })
      .then(() => {
        // We need to cut down the possibility of a memory leak. It is
        // assumed some one will copy-cut-and paste this code, and do
        // something really bad. :grin:
        clearTimeout(id);
      })
  })
  .then(fnLater)

API

Context

Context

Type: React.Context<any>

Provider

Provider

Type: Context.Provider<any>

Consumer

Consumer

Type: Context.Provider<any>

ConfigPropExtenderHoc

Extends React.Component

Use this Wrapper or HOC, Higher Order Component, to copy the config object found in context onto the properties of the first level of children it encapsulates. Because contextType can not be added to JSX functions, you will need to wrap or extend the function to inject the config value. This HOC, simple clones the JSX element, and copies the context's 'config' values as properties.

Parameters

  • children JSX? Optional JSX Children, keep in mind this only attaches the property to all the first level children ( shallow )
  • propName String Optionally you can specify a Property to store the config on, the default is 'config' (optional, default 'config')

Examples

import React from 'react'
import { ConfigPropExtenderHoc } from 'react-static-config-loader'

const ExampleFunctionalDiv = ({ config, someValue }) => <React.Fragment>
 <code>{JSON.stringify(config, null, 4)}</code>
 <div>{someValue}</div>
 </React.Fragment>

const HOCExampleFunctionalDiv = (props) => {
  return (
    <React.Fragment>
      <ConfigPropExtenderHoc>
        <ExampleFunctionalDiv {...props} />
      </ConfigPropExtenderHoc>
    </React.Fragment>
  );
}

 export default HOCExampleFunctionalDiv

Returns JSX

loaderCall

Callback responsible for fetching the external configuration. Because it is a promise, the user can add a 'then' or even use async/await to transform the payload.

Type: Function

Returns Promise<any>

StaticConfigWrapper

StaticConfigWrapper - is everything wrapped up in one JSX tag. I expect this will satisfy the majority of scenarios. However, for those that it does not, the Provider, Consumer, and Context are all broken out. If you find you really need them, this might not be a good solution for your project. redux Action object

Parameters

  • props Object? props the JSX props.

    • props.children JSX.Element All the JSX children, or null. the default value is null. (optional, default null)
    • props.loader loaderCall Required function that will "load" the static configuration returning a promise. It is assumed the function will return a Promise, that can resolve a value or a proper rejection.
    • props.loadingMsg JSX.Element The optional JSX that will be displayed while the loader is running. (optional, default null)

Examples

import React from 'react';
 import { StaticConfigWrapper, Context } from 'react-static-config-loader';
 export class ExampleClass extends React.Component {
  static contextType = Context;
  render() {
    const {someValue} = this.props;
    const config = this.context;
    return <React.Fragment>
      <code>{JSON.stringify(config,null,4)}</code>
      <div>{someValue}</div>
    </React.Fragment>
  }
 }
 const later = async function later(delay, fnLater) {
  return new Promise(function(resolve) {
    setTimeout(resolve, delay);
  }).then(fnLater);
 }
 const App = () => {
  const fn = ()=> Promise.resolve({msg:'go',version:1234,selection:['no','yes'], buttonName:'go go button'})
  return (
    <React.Fragment>
      <StaticConfigWrapper loader={async () => later(2000, fn)}>
        <ExampleClass someValue={'You made it in ExampleClass'}/>
      </StaticConfigWrapper>
    </React.Fragment>
  )
 }
 export default App

Returns JSX.Element

Contributing

Thanks for contributing! 😁 Here are some rules that will make your change to react-static-config-loader fruitful.

Rules

  • Raise a ticket to the feature or bug can be discussed
  • Pull requests are welcome, but must be accompanied by a ticket approved by the repo owner
  • You are expected to add a unit test or two to cover the proposed changes.
  • Please run the tests and make sure tests are all passing before submitting your pull request
  • Do as the Romans do and stick with existing whitespace and formatting conventions (i.e., tabs instead of spaces, etc)
    • we have provided the following: .editorconfig and .eslintrc
    • Don't tamper with or change .editorconfig and .eslintrc
  • Please consider adding an example under examples/ that demonstrates any new functionality

Deployment Steps

These are notes for deploying to NPM. I used npmrc to manage my NPM identities (npm i npmrc -g to install ). Then I created a new profile called public with (npmrc -c public) and then switch to it with npmrc public.

  • create a pull request from dev to main
  • check out main
  • npm version patch -m "message here" or minor
  • npm publish --access public
  • Then switch to dev branch
  • And then merge main into dev and push dev to origin

License

MIT License

Copyright (c) 2021 Philip A Senger

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

MIT © psenger