DEV Community

Cover image for Is global data bad?
Taha Shashtari
Taha Shashtari

Posted on • Edited on • Originally published at tahazsh.com

Is global data bad?

Imagine you are building some widget that contains many nested components in it. At some point, you will need a way to control how this widget should look and work through some config object—especially if you want to publish it as a JavaScript library.

A config object is a perfect case for a global data. But we often hear that global data is bad.

Is it bad? If so, why and what can we do about it?

Why is it bad?

Global data is bad because it can be changed from any place in the project. When something can change in arbitrary, uncontrolled way, then things might break without you noticing.

Global data is harder to reason about: if multiple parts access the same global data (which is what global data is for), then you won't easily know which part has changed it in a way that might break other parts.

The key reason here is mutability. Global data is bad if it's mutable—which means it can be changed. If it's immutable, then it's not that bad because you will be sure that the data is the same everywhere; which means it's easier to reason about.

But sometimes you need the global data to be mutable. In this case, the best thing you can do is to restrict access to it—in other words, to encapsulate it.

An example of unprotected global data

Let's say you store your widget config data in appConfig.js.

// appConfig.js

export const appConfig = {
  maxNumberOfUploads: 5,
  supportedTypes: ['jpg', 'png'],
  isDarkMode: false
}
Enter fullscreen mode Exit fullscreen mode

You can update this config directly from any place in your project.

// main.js

import { appConfig } from './appConfig.js'

appConfig.isDarkMode = true
Enter fullscreen mode Exit fullscreen mode

If you are pretty sure that it will be updated from a single place, then that's fine. But the issue occurs when it can be changed from multiple places. Since appConfig is an object, then it means you can pass it anywhere you want, and it will modify the original object's data; which will make it harder to debug the code and to know what part of your app changed a specific property.

Not only that, but currently there's no way to ensure that the config fields are updated correctly—there's no validation rules for that.

I can improve that by encapsulating the data by providing a getter and a setter for accessing the data with some rules.

Encapsulate mutable global data

The simplest way to restrict access to mutable data is to not export the object itself; instead, export a getter and a setter.

// appConfig.js

let appConfigData = {
  maxNumberOfUploads: 5,
  supportedTypes: ['jpg', 'png'],
  isDarkMode: false
}

export function appConfig() {
  return structuredClone(appConfigData)
}

export function setAppConfig(name, value) {
  if (!appConfigData.hasOwnProperty(name)) {
    throw new Error(`App config does not contain "${name}" option`)
  }
  if (!isValid(name, value)) {
    throw new Error(`Changing ${name} to ${value} is invalid`)
  }
  appConfigData[name] = value
}

function isValid(name, value) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

When exporting the getter, it's important to return a copy of that object so the user can't change the original one. I used structuredClone to clone it—which is great for cloning an object with all of its nested fields.

The setter here takes the name of the config property that you want to change and the new value for it. Since all changes now are done through a function, then you can add any validations before updating the value. For this example, I added a check to make sure the user can't add new properties. And below that, I added a validator for the new value—I added an unimplemented helper function called isValid, which I can implement based on what I want.

If the new config value passes all the checks, then I update the config with the new value—and this time I'm sure it will get a valid value.

Alternative approach: encapsulate the getter with a class

Another option you have for encapsulating mutable data is by wrapping it with a class. This way you'll have more control on the internals of the object; you can choose what fields to provide a setter for—because you might want to disallow changes for some config fields.

Another benefit for this approach is that you can group the validation code with its field setter—which is much cleaner.

// appConfig.js

let appConfigData = {
  maxNumberOfUploads: 5,
  supportedTypes: ['jpg', 'png'],
  isDarkMode: false
}

class AppConfig {
  #maxNumberOfUploads
  #supportedTypes
  #isDarkMode

  constructor(data) {
    this.#maxNumberOfUploads = data.maxNumberOfUploads
    this.#supportedTypes = data.supportedTypes
    this.#isDarkMode = data.isDarkMode
  }

  get maxNumberOfUploads() {
    return this.#maxNumberOfUploads
  }

  set maxNumberOfUploads(value) {
    if (!Number.isNaN(value)) {
      throw new Error('maxNumberOfUploads must be a number')
    }

    this.#maxNumberOfUploads = value
  }

  get isDarkMode() { //... }
  set isDarkMode() { //... }

  get supportedTypes() { //... }
}

export function appConfig() {
  const clonedConfig = structuredClone(appConfigData)
  return new AppConfig(clonedConfig)
}

export function setAppConfig(newConfig) {
  appConfigData = newConfig
}
Enter fullscreen mode Exit fullscreen mode

In this version, the getter is still cloning the config data, but it's wrapping it with a new instance of AppConfig. I also changed the setter to take a new config object instead of name and value.

Updating the config would look like this:

// main.js

import { appConfig, setAppConfig } from 'appConfig'

const config = appConfig()
config.isDarkMode = true

setAppConfig(config)
Enter fullscreen mode Exit fullscreen mode

I prefer this approach because the code has a better structure. For example, I didn't want to allow the user to change supportedTypes, so I just didn't provide a setter for it. Also, look how easy it's to validate each field—I just add the validation code in the field's setter.

Conclusion

If you can implement your app without global data, then that's great. You should try to avoid global data as much as you can—especially if they are mutable.

However, if they are needed, then restrict access to them by encapsulating them. When you encapsulate your global data, you can control what parts can change and how they change.

Top comments (7)

Collapse
 
ant_f_dev profile image
Anthony Fung

I like the idea of exporting a wrapper of the data - it's similar to creating repositories to read and write the data. While it doesn't stop updating the data from anywhere, I appreciate that it's possible to:

  • breakpoint the setter function at runtime
  • add validation

As a bonus, it becomes possible to mock it in unit tests.

If immutability is the ultimate goal, it might be worth looking at libraries like Immutable.js

Collapse
 
efpage profile image
Eckehard

Another bad practice is to have a double responsibilty for data.

Assume, you have a function that relies on a certain data structure. If you change the function, you need to change the data structure too. Changing only one side may lead to unwanted effects.

That was one reason people used OO-Classes, where functions and data are combined in a relatively closed environment. How do you avoid these type of errors in functional coding?

Collapse
 
tahazsh profile image
Taha Shashtari

Your question is about coupling. Coupling is a general concept that can be applied to any programming paradigm (OO or functional). Sometimes some parts of your software needs to be coupled if they always depend on each other. In this case your code will be more cohesive, which is a good thing. More cohesive means that the things that must depend on each other should be grouped together and changed together.

In this example, the data structure and the functions working on them are grouped together under the same module (ES module). These functions only work with this data structure, and they are not intended to be used with something else outside that scope.

In this example, I encapsulated the global data to restrict the access to it. This gives me a lot of benefits, but also requires me to add more code to control its access. This is a trade off, but it's worth it in this case. If I need to update the fields of that config object, I know exactly where to do all the related changes. All changes are clear and will be done in the same place (in appConfig.js module for this example). I don't need to update any other files as long as the appConfig and setAppConfig function names and parameters remain the same—and they are not expected to change in this example.

So whether you group your data structure and its functions in a class or a module, the data structure and its functions will be coupled to each other in a clear way. It's a minor trade off, but it will give you the benefits of restricting access to the global data to prevent potential bugs and to add validations.

Collapse
 
efpage profile image
Eckehard

In OO you have generally no chance to separate methods and properties, as they usually are defined in the same class.

But what, if you use tools like Redux? And what about states that need to be used all across your application, like the language setting? How do you care about unwanted sideeffects in a larger project?

Collapse
 
corners2wall profile image
Corners 2 Wall

I can't understand why people do this. In my view, below line is example of conscious change property of object.
appConfig.isDarkMode = true

Collapse
 
tahazsh profile image
Taha Shashtari

Since appConfig is a mutable object, then you won't be sure how and where it's being modified in your app. This is true for all kind of objects because they are passed-by reference, which means changing it any where in your code will change the original one.

If you are building a very simple project that you can easily see how that object is used, then there's no issue with modifying the object directly; the issue happens when your app is more complex and that object will be used in a lot of places. When this is the case, then it will be harder to know what has caused the latest changes to your object—because remember it's a reference value and it might be passed somewhere where it's not obvious if it changed it or not (it might be a function that changes the passed data in a way you don't want it to). But when you restrict the access to it by encapsulating it, then you will know what changes it and what just reads it.

Another extra benefit is that you now have a place to add validation before updating each field—or maybe you want to prevent updating some fields.

Also, please note that all depend on the scope of that object. If it has a small scope, then maybe it's not worth encapsulating it as it will be easy to reason about. But things become harder to reason about when it has a larger scope—and in this case it has the largest scope because it's global.

I hope my answer was helpful to you.

Collapse
 
corners2wall profile image
Corners 2 Wall

Thank you for your advice. Really good explanation and it becomes clearer point with references and encapsulation.