Our technology stack for neeto
We are building neeto and our technology stack is quite simple. On the front end, we use React.js. On the backend we use Ruby on Rails, PostgreSQL, Redis and Sidekiq.
The term globalProps might not ring a bell for most people. It was coined by the BigBinary team for our internal use. globalProps is data that is directly retrieved from our backend and assigned to the browser global object that's window. To view the global props, we can type globalProps in the browser console, which prints out useful information set by the backend service.
How is globalProps implemented
To understand where the globalProps came from and how it works, we need to examine the React-Rails gem. It uses Ruby on Rails asset pipeline to automatically transform JSX into Ruby on Rails compatible assets using the Ruby Babel transpiler.
react_component helper method takes a component name as the first argument, props as the second argument and a list of HTML attributes as the third argument. The documentation has more details.
1<%= react_component("App", get_client_props, { class: "root-container" }) %>
Limitations of the default behavior
In the react-rails gem, the hash is set as the props of the component specified in the react_component method by default. In the example above, the hash returned by the get_client_props method is passed as props to the App component in the front end.
The limitation of this approach is that we need to pass down globalProps through all the components by prop-drilling or React Context.
The concept of nanos
At neeto, anything that does not contain product-specific business logic and can be extracted into a reusable tool is extracted into an independent package. We call them nanos.
You can read more about it in our blog on how nanos make Neeto better.
Limitations in accessing the props by nanos and utility functions
The issue with the above approaches in handling the props is that it won't be directly available in utility functions or nano. We explicitly need to pass it as arguments to utility functions after prop drilling. If we use React Context, it can only be accessed in React components or hooks, it cannot be accessed in utility functions. Also, we cannot directly obtain the reference of the Context within the nanos.
Why we didn't chose environment variables
Some of the variables inside the globalProps are environment variables, which is usually accessed as process.env.VARIABLE_NAME. If we set environment variables, they will be hardcoded into the JavaScript bundle at the time of bundling. This implies that whenever we need to change the environment variable, we must trigger a redeployment.
The solution is globalProps
The advantage of globalProps over these approaches is, it's accessible everywhere since it's in the browser global object window. All the nanos and utility functions that we integrate into the application have seamless access to the props without any extra step of wiring.
Seeding the hash at the backend into the browser's global object, window
Seeding the hash at the backend into the browser's global object, window, is accomplished using the above-mentioned helper method, react_component. As we discussed earlier, an HTML node is created that contains data-react-class representing the component name anddata-react-props attribute representing the hash we passed from the backend as an HTML-encoded string.
Decode the HTML-encoded string into JavaScript object
The next step is to decode the HTML-encoded string and parse it into a JavaScript object. The hash is read from the root-container HTML node and parsed into a JavaScript object.
1const rootContainer = document.getElementsByClassName("root-container")[0]; 2 3const reactProps = JSON.parse(rootContainer?.dataset?.reactProps || "{}");
Convert the case of the keys in the object
The JavaScript object that we have obtained have the keys in snake case. We will convert them into camel case using the helper method keysToCamelCase.
We convert the case because React prefers camel case keys, while Rails prefers snake case keys.
1const globalProps = keysToCamelCase(reactProps);
Deepfreeze the global props
Additionally, we take an extra step to deep freeze the global props object, ensuring immutability. The helper function used here is deepFreezeObject. This prevents modifications to global props from within the product and thus ensures data integrity when working with different nanos. All these steps are performed before the initial rendering of the React component.
1window.globalProps = globalProps; 2 3deepFreezeObject(window.globalProps);
Let's see the final codeblock that seeds the hash at the backend to the browser's global object.
1export default function initializeGlobalProps() { 2 const rootContainer = document.getElementsByClassName("root-container"); 3 const htmlEncodedReactProps = rootContainer[0]?.dataset?.reactProps; 4 5 const reactProps = JSON.parse(htmlEncodedReactProps || "{}"); 6 const globalProps = keysToCamelCase(reactProps); 7 8 window.globalProps = globalProps; 9 deepFreezeObject(window.globalProps); 10}
If we take a closer look at the content of globalProps, we can see that it carries a lot of data. As discussed earlier this data is useful to other nanos. Some of the data being passed are appName, honeyBadgerApiKey, organization, user info, etc.