Logging in React | In-Depth Guide & Tutorial
How to effectively log info and error messages within a React application, and the best practices for doing this.

We all start with writing a bunch of console.log
statements in our work-in-progress React components. But then, you find yourself tracking these guys down because it’s not great to have users see all the console logs in a production app.
Here, we’ll see that with a little structure we can start transforming a bad habit into a powerful best practice.
1. Our Little React Sandbox
Let’s quickly create a React app to play with, using yarn
, create-react-app
and Typescript:
yarn create react-app --template typescript react-logging-start
cd react-logging-start
yarn start
We can now start playing around in src/App.tsx
and add our usual console.log()
statements.
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
console.log('Hello!');
console.warn('Careful there!');
console.error('Oh, no!');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
These should appear in your browser’s developer console:
In this case, it would be simple to remove or comment out the console.log()
line before pushing the code to production, but this can become tedious as your application grows.
So what should we do? Let’s see how we can keep the log statements but suppress the outputs by building a simple wrapper around the console
object.
2. Our Own Logging Wrapper
Let’s start by simply replicating the console
functions.
import React from 'react';
import './App.css';
import { ConsoleLogger } from './logger';
import logo from './logo.svg';
function App() {
const logger = new ConsoleLogger();
logger.log('Hello!');
logger.warn('Careful there!');
logger.error('Oh, no!');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
/** Signature of a logging function */
export interface LogFn {
(message?: any, ...optionalParams: any[]): void;
}
/** Basic logger interface */
export interface Logger {
log: LogFn;
warn: LogFn;
error: LogFn;
}
/** Logger which outputs to the browser console */
export class ConsoleLogger implements Logger {
log(message?: any, ...optionalParams: any[]): void {
console.log(message, ...optionalParams);
}
warn(message?: any, ...optionalParams: any[]): void {
console.warn(message, ...optionalParams);
}
error(message?: any, ...optionalParams: any[]): void {
console.error(message, ...optionalParams);
}
}
Ah, but now we’ve lost the correct source file mapping in the browser console:
To preserve the correct source file lines, we can reuse the console functions themselves.
/** Logger which outputs to the browser console */
export class ConsoleLogger implements Logger {
readonly log: LogFn;
readonly warn: LogFn;
readonly error: LogFn;
constructor() {
this.log = console.log.bind(console);
this.warn = console.warn.bind(console);
this.error = console.error.bind(console);
}
}/** Logger which outputs to the browser console */
export class ConsoleLogger implements Logger {
readonly log: LogFn;
readonly warn: LogFn;
readonly error: LogFn;
constructor() {
this.log = console.log.bind(console);
this.warn = console.warn.bind(console);
this.error = console.error.bind(console);
}
}
Now, we’re back to where we started:
Wait, wait, wait! There’s a reason we did all that work.
Now, we can customize our own console logger to do what we need.
3. Let’s Level Up Our Logging Game
The first thing we can do is to make our ConsoleLogger
configurable, we can tell it to output all logs, only the warn
and error
logs, or the error
logs only.
/** Signature of a logging function */
export interface LogFn {
(message?: any, ...optionalParams: any[]): void;
}
/** Basic logger interface */
export interface Logger {
log: LogFn;
warn: LogFn;
error: LogFn;
}
/** Log levels */
export type LogLevel = 'log' | 'warn' | 'error';
const NO_OP: LogFn = (message?: any, ...optionalParams: any[]) => {};
/** Logger which outputs to the browser console */
export class ConsoleLogger implements Logger {
readonly log: LogFn;
readonly warn: LogFn;
readonly error: LogFn;
constructor(options?: { level? : LogLevel }) {
const { level } = options || {};
this.error = console.error.bind(console);
if (level === 'error') {
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.warn = console.warn.bind(console);
if (level === 'warn') {
this.log = NO_OP;
return;
}
this.log = console.log.bind(console);
}
}
We can see that setting the level
option to error
will make it drop log
and warn
messages while using the warn level will make it drop log messages only.
The log level option is also optional, in which case the log
level is used, i.e. all messages are outputted to the browser console.
Now we can try it by editing src/App.tsx
and setting the level option. Using the warn
level, there should now be only two logs printed in the console:
const logger = new ConsoleLogger({ level: 'warn' });
“But I still need to find and replace all those ConsoleLogger
thingies before pushing to production!”
Ah, yes… But you can also create a global logger options object to help you:
import React from 'react';
import './App.css';
import { LOG_LEVEL } from './environment';
import { ConsoleLogger } from './logger';
import logo from './logo.svg';
function App() {
const logger = new ConsoleLogger({ level: LOG_LEVEL });
logger.log('Hello!');
logger.warn('Careful there!');
logger.error('Oh, no!');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
import { LogLevel } from "./logger";
/** The App environment */
export type Environment = 'development' | 'production';
export const APP_ENV: Environment = process.env.REACT_APP_APP_ENV === 'production' ? 'production' : 'development';
export const LOG_LEVEL: LogLevel = APP_ENV === 'production' ? 'warn' : 'log';
Now, you can see the difference when running the app:
yarn start # Will show all logs
REACT_APP_APP_ENV='production' yarn start # Will show only 'warn' and
'error' logs
# Remember to build the production App with:
REACT_APP_APP_ENV='production' yarn build
If you’ve managed to stay on the ride with me up till now, you know you can already use logger.log()
everywhere and know it will be removed in the production builds.
Note: You can replace process.env.REACT_APP_APP_ENV
with process.env.NODE_ENV
to have log levels configured by create-react-app (see docs).
4. Exporting the Logger
One last step we can do is to export the logger
so that we can simply import it and reuse it in all the components of our React application.
import React from 'react';
import './App.css';
import { logger } from './logger';
import logo from './logo.svg';
function App() {
logger.log('Hello!');
logger.warn('Careful there!');
logger.error('Oh, no!');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
import { LogLevel } from "./logger";
/** The App environment */
export type Environment = 'development' | 'production';
export const APP_ENV: Environment = process.env.REACT_APP_APP_ENV === 'production' ? 'production' : 'development';
export const LOG_LEVEL: LogLevel = APP_ENV === 'production' ? 'warn' : 'log';
import { LOG_LEVEL } from "./environment";
/** Signature of a logging function */
export interface LogFn {
(message?: any, ...optionalParams: any[]): void;
}
/** Basic logger interface */
export interface Logger {
log: LogFn;
warn: LogFn;
error: LogFn;
}
/** Log levels */
export type LogLevel = 'log' | 'warn' | 'error';
const NO_OP: LogFn = (message?: any, ...optionalParams: any[]) => {};
/** Logger which outputs to the browser console */
export class ConsoleLogger implements Logger {
readonly log: LogFn;
readonly warn: LogFn;
readonly error: LogFn;
constructor(options?: { level? : LogLevel }) {
const { level } = options || {};
this.error = console.error.bind(console);
if (level === 'error') {
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.warn = console.warn.bind(console);
if (level === 'warn') {
this.log = NO_OP;
return;
}
this.log = console.log.bind(console);
}
}
export const logger = new ConsoleLogger({ level: LOG_LEVEL });
As you can see, we can now add import { logger } from './logger'
;
everywhere we want to use our logger, without the need to create a new instance each and every time.
Conclusion
Wow, you made it!
I understand your enthusiasm 😂️. While this may not look like much right now, it’s still a nice start towards good logging hygiene.
With our quick setup we can now replace all the console.log()
statements we have with logger.log()
and we won’t have to track them down before building a production app. This encourages you to still use logging statements while developing.
Next Steps
What can you do from here?
-
As your application grows, you may find that keeping all
log()
statements in development is becoming too verbose. You can disable thelog
level in development too and use it only in the components you’re working on (by creating a newlogger
instance). -
You can add logging to Sentry or other monitoring platforms in production. This will allow you to capture
warning
anderror
events even when they don’t crash your application.
Meticulous
Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.
Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.
Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.
Learn more here.
Authored by Johann-Michael Thiebaut, engineer at Meticulous