curly braces revolution

Fine, react. How much we use everything, because it happened historically: we write functions backwards (first declare, then call). We wrap everything in arrow functions and constants with them when a named function is enough. This list could well go on.

In this article, I’d like to discuss a few habits that we all have that might need to be rethought.

Let’s start with custom hooks that need to return a result, in particular a couple of values. A classic example would be a hook to request data over the network and see if that request is currently in progress in order to add a loader to the page.

function useClient(clientId) {
  const [loadedClient, setLoadedClient] = useState(null)
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    const loadClient = async () => {
      try {
        setIsLoading(true)
        const clientData = {} // actual data loading call goes here
        setLoadedClient(clientData)
      }
      finally {
        setIsLoading(false)
      }
    }

    loadClient()
  }, [clientId])

  return [loadedClient, isLoading]
}

As a result, we call it something like this:

const [loadedClient, isLoading] = useClient(clientId)

Or maybe like this:

const [client, isClientLoading] = useClient(clientId)

Or we make a mistake and write something like this:

const [isLoading, loadedClient] = useClient(clientId)

It seems that here we are guided by the pattern from useState, when it returns us an array of values ​​and a function to update it, and then we destructure the array and get everything by index, give it any names, cool.

Why did you have to do this for useState?

Because it is a library generic function. Clients call it on their specific occasions and want to give different, useful, meaningful names.

So what happens with our custom useClient hook?

We follow someone’s convention, because everyone does it, which means it’s a react way and stuff like that.

But do we need this flexibility in terms of naming the results of this function? And if we do not have TypeScript, then who will help to suspect something is wrong? And if we search for all uses by a full search in the project without using useful IDE chips, then how much will we find if everyone calls it what they want?

There is a simple suggestion. Let’s return an object, not an array. In this case, the last line in the hook would be:

return {loadedClient, isLoading}

And we will use it like this:

const {loadedClient, isLoading} = useClient(clientId)

Or like this:

const {isLoading, loadedClient} = useClient(clientId)

But this is no longer possible, or maybe even a linter or some other animal will tell us that there is an error here:

const {loading, c} = useClient(clientId)

What are the main conclusions from this example:

  1. The change in the code is minimal and does not affect the complexity of the solution in any way.

  2. Relatively large type safety of our code.

  3. All the same destructuring goodies, only not on arrays, but on objects.

  4. You can safely refactor the code: rearrange the returned properties inside the hook and in the places of use. After all, the order is no longer important, unlike the destructuring of arrays.

Moving on to more or less immutable rule: if only one thing is exported from the module, you need to use the default export.

How it might look in terms of the module itself:

export default function myFancyFunction() {}

or:

function myFancyFunction() {}
export default myFancyFunction

or:

const myFancyFunction = () => {}
export default myFancyFunction

And if we try to use such a module somewhere, then most likely we will write it like this:

import myFancyFunction from './my-fancy-function'

And if they disliked us, they will import it like this:

import myFancy from './my-fancy-function'

In general, it should already be looming where we are going. Namely, the same as in the first example: we return something that can be called by the client whatever you like. And if we are not developing an npm package, but working within the same repository on a project, is this what we want? Such flexibility?

Perhaps it’s time to break the postulate that using the default export for a single-function module is a must.

Named export, it’s your time! Let’s always use you.

In this case, we get:

export {
  myFancyFunction,
}

function myFancyFunction() {}

And then we import like this:

import {
  myFancyFunction,
} from './my-fancy-function'

An interesting point: if you always use a named export, then adding a new function exported from the module is, in fact, free.

And we are gradually starting to move to multi-line destructuring. When one thing goes on its own line and Necessarily ends with a comma. But why? After all, a comma is not required for the last element. In general, multi-line destructuring results in more lines of code.

Note: there is a strange behavior in VSCode – the default export is not always renamed normally by the IDE. Maybe something is wrong with us, or maybe something is just more difficult to do for default exports. With named problems it is not noticed yet.

There are several reasons to do this wherever there is object destructuring or something like it, as in the case of named imports:

  1. A clean commit history in the git when a new property needs to be added or a new feature needs to be exported.

  1. You can build new code from old code faster than ever. You copy the previous line, hop, and here you have the same new one, you rename it.

What other interesting thing can you try to add to this feast of curly braces? Let’s add here RORO pattern (Request Object Response Object). In other words, we always pass arguments as an object, even for a single parameter. We always return the result as an object.


Let’s try to combine everything we had in our custom hook and see what happens

export {
  useClient,
}

function useClient({
  clientId,
}) {
  const [loadedClient, setLoadedClient] = useState(null)
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    const loadClient = async () => {
      try {
        setIsLoading(true)
        const clientData = {} // actual data loading call goes here
        setLoadedClient(clientData)
      }
      finally {
        setIsLoading(false)
      }
    }

    loadClient()
  }, [clientId])

  return {
    loadedClient, 
    isLoading,
  }
}

And usage:

import {
  useClient,
} from './useClient'

const {
  loadedClient,
  isLoading,
} = useClient({
  clientId,
})

conclusions

In general, as it turned out, the article is not about React at all. This approach applies to any ES6+ code written in JavaScript. It’s just that using the example of custom react hooks is easier to illustrate the value.

We ourselves apply this approach everywhere (okay, we’re about to start). There is one reasonable exception to the Response Object from the RORO pattern. It doesn’t always seem to make sense to wrap everything in an object – for example, is it worth it for functions that return true/false flags? Perhaps this is an overkill:

const {
  areTheyNuts,
} = areTheyNuts()

As for the ubiquitous return of objects as the results of functions, we are undecided. Perhaps it is worth practicing the extreme option on a small project, when we always return the object, and only then the team decides whether this option suits you personally or not.

If you do this and nothing else:

  1. People may not understand. This seems to be very different from what people repeat like a mantra in code.

  1. You are not afraid to extend the function signature by adding a new argument. You don’t have to rewrite all the code. And even a new argument with a default value will not be a problem, unlike passing arguments one at a time, when all arguments with a default value must come at the end.

Earlier:

Now:

Here we can substitute a new argument in the middle and do not have to change anything. Wherever we want, we add there – and there are no requirements for a fixed order, as with passing multiple arguments.

function welcomeClient({
  name,
  phoneNumber="",
  email,
  company,
  trialVersion = true,
})

And we call like this:

welcomeClient({
  name: 'Tom',
  phoneNumber: '+79876543210',
  email: 'tom@tourmalinecore.com',
  company: 'Tourmaline Core',
})
  1. The module export extension is again easy to do, none of its clients will need to be updated. As they imported, they will continue.

  2. The history in Git will look clean and tidy. Only the line that has actually been changed will be highlighted, i.e. the line with a new result property/property of the input parameters object or a new function for export/function for import.

  3. It seems (not certain) that IDEs do a better job of renaming object properties and module exports using this approach. Although VSCode sometimes stops halfway and this is not what you need.

Case 1. If we rename a function in the module from which it is exported, then this does not affect the use in any way, there is still the same name as before the renaming.

Case 1 If we rename the function in the module from which it is exported, then this does not affect the usage in any way, it is still the same name as before the renaming.

Case 2. If we rename the properties of an object during destructuring.

Case 2 If we rename the properties of the object during destructuring.

Case 3. If we rename the function in the place where it is imported, then this does not affect its name in the module from which it is exported.

Case 3 If we rename the function in the place where it is imported, then this does not affect its name in the module from which it is exported.

This is a backward compatible change, but usually we need to rename in all places at once, which is inconvenient.

PS: By the way, if you use ESLint or Prettier, then the miracle will not happen, the proposed multiline formatting has not yet been implemented. Excellent contributor candidate, only 6 years old at the time of publication https://github.com/prettier/prettier/issues/2550.


Author: Shinkarev Alexander

Proofreading and feedback: Chekina Ekaterina, Yasnovsky Andrey, Yadryshnikova Maria

Article design: Anastasia Kovylyaeva, Anastasia Tupikina

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *