February 6, 2024
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.
globalProps
implementedTo 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.
<%= react_component("App", get_client_props, { class: "root-container" }) %>
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.
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.
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.
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.
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, 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.
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.
const rootContainer = document.getElementsByClassName("root-container")[0];
const reactProps = JSON.parse(rootContainer?.dataset?.reactProps || "{}");
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.
const globalProps = keysToCamelCase(reactProps);
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.
window.globalProps = globalProps;
deepFreezeObject(window.globalProps);
Let's see the final codeblock that seeds the hash at the backend to the browser's global object.
export default function initializeGlobalProps() {
const rootContainer = document.getElementsByClassName("root-container");
const htmlEncodedReactProps = rootContainer[0]?.dataset?.reactProps;
const reactProps = JSON.parse(htmlEncodedReactProps || "{}");
const globalProps = keysToCamelCase(reactProps);
window.globalProps = globalProps;
deepFreezeObject(window.globalProps);
}
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.
If this blog was helpful, check out our full blog archive.