Zane Claes
Zane Claes

Reputation: 14954

How to access the functional component's name from a React hook?

I am trying to write a custom React hook, useLogging, where I would like to contextualize the log message based upon the name of the component which is doing the logging.

For example:

const Login: React.FunctionComponent<IProps> = (props) => {
  log = useLogging();

  log.info("Hello!")
 

  [...]

Should produce [Login] Hello!

My custom hook, then, needs the name Login:

export const useLogger = () => {
  // "this" is undefined
  const loggerName = ??????
  return logManager.getLogger(loggerName);
};

In the context of a class, what I'm looking for is something like this.constructor.displayName. However, a React hook does not have this set, and I cannot seem to find documentation on obtaining reference to the functional component's context.

—-

Edit: I would prefer not to pass any arguments and not to add a bunch of boiler plate. My goal is that the useLogging() function will survive component refactoring, and not rely upon the developer to provide a "correct" name.

Upvotes: 11

Views: 7944

Answers (1)

cbr
cbr

Reputation: 13652

There's a couple other ways that I can think of that you could use. The first and the simplest the one Drew suggested in the comment, which is to simply pass the logging name as an argument:

const useLogger = (name: string) => {
  return logManager.getLogger(name)
}

const Login: React.FC<Props> = () => {
  const log = useLogger('Login')
  // ...
}

You could also obtain the name via .displayName or .name. Notice that .name refers to Function.name, which, if you're using webpack, will probably be minified in a production build so you'll end up with names like "t" or "s" etc. If you need the same name as in your component, you can assign displayName and let the hook take care of it:

const useLogger = (component: React.ComponentType<any>) => {
  const name = useLogger(component.displayName || component.name);
  return logManager.getLogger(name);
}

const Login: React.FC<Props> = () => {
  const log = useLogger(Login)
}
Login.displayName = 'Login';

If you're okay with passing a name to useLogger, but don't want to set displayName every time, you could use something like ts-nameof which aims to give you a nameof operator like in C#:

const useLogger = (name: string) => {
  return logManager.getLogger(name)
}

const Login: React.FC<Props> = () => {
  const log = useLogger(nameof(Login))
  // ...
}

The upside here is that the name will survive auto-renames. This requires some bundler or Babel configuration. I haven't tested how minification affects this, but there's three different flavors of ts-nameof (at the time of writing) which you can use:

Pick the first one that matches your build pipeline.


Alternatively, if the logger isn't component-specific, but module-specific, you could make a factory for the hook, and initialize it once at the top of your module:

const makeUseLogger = (name: string) => () => {
  return logManager.getLogger(name)
}

// in your module

const useLogger = makeUseLogger('Module name')

const Login: React.FC<Props> = () => {
  const log = useLogger()
  // ...
}

As an extension of this, if the logger itself doesn't actually need to be a hook (doesn't use other hooks or need props etc.), just make a logger for your module at the top level directly:

const log = logManager.getLogger('Module name')

const Login: React.FC<Props> = () => {
  log.info('hello')
}

Additionally, if you don't mind your project's directory structure leaking into a production build, you can use a webpack trick:

// webpack.config.js
module.exports = {
  // ...
  node: {
    __filename: true
  }
}

and then

const log = logManager.getLogger(__filename)

In a file whose path is /home/user/project/src/components/Login.ts, and the webpack context is /home/user/project, the __filename variable will resolve to be src/components/Login.ts.

Although, this will probably require you to create a typedef e.g. globals.d.ts where you declare the __filename global for Typescript:

declare global {
  __filename: string;
}

Note: this will not work if your build target is umd.


As a side-note, technically, if you for some reason don't want to pass any args to useLogging, you could use the deprecated Function.caller property, e.g.

function useLogging() {
  const caller = (useLogging.caller as React.ComponentType<any>);
  const name = caller.displayName || caller.name;
  console.log(name);
  return logManager.getLogger(name);
}

const Login: React.FC<Props> = () => {
   const log = useLogging()
   // ...
}

However, that property is deprecated so you'll have to clean that up sooner or later, so don't do that in production code.

Upvotes: 15

Related Questions