7

Suppose I am writing an application in Redux and I am tasked to add logging using a 3rd party library. Its API is as follows:

function createLogger(token) {
    // the logger has internal state!
    let logCount = 0;

    return {
        log(payload) {
            logCount++;            // modify local state

            fetch('/someapi', {    // ship payload to some API
                method: 'POST',
                body: payload
            });
        }
    };
}

I would then use the library something like this:

let logger = createLogger('xyz');
logger.log('foobar');

I definitely want to create the logger instance just once during application init. But then the question is: where do I store the logger instance?

First instict is to put it somewhere in the store. But is that a good idea? As I have demonstrated in the code the logger object is stateful, it stores a counter in the closure. I do not get a new instance like I would with an immutable object. As we know, state should only be modified via pure reducer functions.

Other possibilities are to create the instance somewhere in a redux middleware closure or just create a global variable, which is obviously evil in terms of testability.

Is there a best practice for this (I would think) rather common scenario?

VoY
  • 5,479
  • 2
  • 37
  • 45
  • Are you using ES6 modules? Will components `log` calls directly, or is logging done automatically by the store/actions? – Kyeotic Dec 08 '15 at 21:02
  • Yes, I am using ES6 modules. As for the other questions I would expect the natural place for these calls to be the action creators via something like redux-thunk. I don't want my components to cause side-effects. – VoY Dec 08 '15 at 21:14
  • If the logger itself causes no side effects on the state of the application itself, then it probably belongs in middleware. A few things remain unclear from your code however. (1) What's the purpose of the token parameter? (2) What's the purpose of the logCount variable? (Neither token nor logCount is included in the post request.) (3) Finally, what's the payload? Is it derived from the action and/or state? – David L. Walsh Dec 09 '15 at 02:53
  • (1) That's just some configuration of the logger object. I can imagine that the object itself does some async initialization using that parameter (e.g. logging in, then returning logger synchronously and queue log calls after login has finished). (2) Of course in itself it makes no sense, but I used it to show that logger has some sort of internal state, that it changes over time without me necessarily getting a new instance. (3) In practice that would likely be some message derived from the action and/or state. Putting it in middleware sounds best to me for now... thank you for your comment! – VoY Dec 09 '15 at 07:31

2 Answers2

4

Since you are using ES6 modules I would setup your logger as a module, export it, and import it wherever you plan to use it. I think logging from the actions is a solid plan, since it keeps the components unaware, and doesn't pollute the store with side-effects.

function createLogger(token) {
    // the logger has internal state!
    let logCount = 0;

    return {
        log(payload) {
            logCount++;            // modify local state

            fetch('/someapi', {    // ship payload to some API
                method: 'POST',
                body: payload
            });
        }
    };
}

export default const logger = createLogger('xyz');

Your action creators

import logger from 'logger-module';

//
logger.log('somestuff');

Testing is still easily achievable by importing the logger and placing whatever spy/stub on its methods that you need to intercept.

Kyeotic
  • 19,697
  • 10
  • 71
  • 128
  • So basically you would test actions using the logger by utilizing some library that overrides that makes the import stateement import you mock instead of the real logger, did I understand you correctly? – VoY Dec 08 '15 at 21:45
  • You can still import the real logger. As long as its methods are spy/stub replaced before it is used, the calls will still get intercepted. Its just a javascript object, and spying/stubing replaces its properties. Its just standard testing practices. – Kyeotic Dec 08 '15 at 21:47
  • To me that's in a way similar to the global variable, expect you can relatively easily swap the implementation. But you would not be able to create two independent instances of the redux application as you only ever get one module level logger. – VoY Dec 08 '15 at 21:47
  • 1
    If that was necessary, you could export a logger factory that returned loggers based on keys. Then each app could request a logger using a generated key, and they would each have their own singleton. We are starting to get into the weeds though. – Kyeotic Dec 08 '15 at 21:49
0

From the Redux documentation:

/**
 * Sends crash reports as state is updated and listeners are notified.
 */
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

Raven being a third-party library.

If the library has its own state then it shouldn't be an issue using it in middleware (the state belongs in the library and not your app). If you're creating a state for it, for some reason, then that state should belong in the Redux store, probably under store.logger or something.

Mike
  • 1,442
  • 10
  • 20