Storybook: Log React Router <Link/> clicks as actions?

Created on 23 Sep 2016  路  14Comments  路  Source: storybookjs/storybook

I'm looking for a way to log clicks on <Link/> components (from [email protected]) as actions. Currently a click throws this error: "Uncaught Invariant Violation: s rendered outside of a router context cannot navigate."

Is there a way to do this already? I couldn't find anything

The links addon is interesting, but a completely unrelated usecase.

discussion

Most helpful comment

Here's the "correct" way to replace React-Router's history functions with actions. This is in my config.js:

import { addDecorator, action } from '@kadira/storybook'
import { Router } from 'react-router'
import createMemoryHistory from 'history/createMemoryHistory'

const history = createMemoryHistory()

history.push = action('history.push')
history.replace = action('history.replace')
history.go = action('history.go')
history.goBack = action('history.goBack')
history.goForward = action('history.goForward')

addDecorator(story => <Router history={history}>{story()}</Router>)

All 14 comments

@jzaefferer First of all, link addon is a totally different one.
This is because, we are using the RR outside of the it's router context.

We need to find a proper way to deal with the <Link/>. One way is to put a custom webpack alias and set a mock to <Link>.

Or you can new Link component wrapping the original with react-stubber.

I tried this, here's how to do this:

First create a file called rr.js inside your Storybook config directory (.storybook) with the following content:

import React from 'react';
import { action } from '@kadira/storybook';

// Export the original react-router
module.exports = require('react-router-original');

// Set the custom link component.
module.exports.Link = class Link extends React.Component {
  handleClick(e) {
    e.preventDefault();
    const { to } = this.props;
    action('Link')(to);
  }

  render() {
    const { children, style } = this.props;

    return (
      <a
        style={style}
        href='#'
        onClick={(e) => this.handleClick(e)}
      >
        {children}
      </a>
    );
  }
};

Then create another file called webpack.config.js and add following content:

// load the default config generator.
var genDefaultConfig = require('@kadira/storybook/dist/server/config/defaults/webpack.config.js');

module.exports = function(config, env) {
  // You can use your own config here as well, instead our default config.
  var config = genDefaultConfig(config, env);

  // this is used by our custome `rr.js` module
  config.resolve.alias['react-router-original'] = require.resolve('react-router');
  // this `rr.js` will replace the Link with a our own mock component.
  config.resolve.alias['react-router'] = require.resolve('./rr.js');
  return config;
};

Thank you! I will give that a try.

@jzaefferer how was it?

Can you use this method to use the linkTo() function for <Link /> components?

I tried this, but couldn't get it working. Apparently the aliased rr.js is never loaded, so the custom Link implementation isn't loaded either. I couldn't figure out how to debug the resolve.alias besides outputting the resolved path for rr.js (which was correct).

Any ideas what I might be missing? I'm hardly an expert on webpack :/

Would it not be better to use a custom React Router "provider" and wrap all stories in it. Apologies for shooting from the hip, but if we had a way to add a decorator for all stories (do we?), and plugins were able to add them, then we would make a plugin to do the above..

@tmeasday That strikes me as a much better idea, I'm going to play around with it and see what I can come up with.

Here's the "correct" way to replace React-Router's history functions with actions. This is in my config.js:

import { addDecorator, action } from '@kadira/storybook'
import { Router } from 'react-router'
import createMemoryHistory from 'history/createMemoryHistory'

const history = createMemoryHistory()

history.push = action('history.push')
history.replace = action('history.replace')
history.go = action('history.go')
history.goBack = action('history.goBack')
history.goForward = action('history.goForward')

addDecorator(story => <Router history={history}>{story()}</Router>)

Since I don't have history as a dependency, I imported createMemoryHistory from react-router:

import { Router, createMemoryHistory } from 'react-router'

Otherwise I tried it as you suggested. I get these "warnings" (really errors):

Warning: [react-router] Location "/" did not match any routes
Warning: [react-router] You cannot change <Router routes>; it will be ignored

Using [email protected].

Trying to resolve that, I came up with this:

addDecorator(story => <Router history={history}>
  <Route path='/' component={story} />
</Router>)

That actually works, logging the actions, but I still get the "You cannot change ; it will be ignored" warning/error and I have no idea where it comes from.

Starting from this discussion, I've created a decorator that logs react-router v4 links and replace them with a linkTo if configured.

The decorator is actually an HOC that accepts an object (containing the links to replace) and wraps the actual decorator to create a component that performs the real replacement. The decorator code can be found here, and it is used this way:

.addDecorator(StoryRouter({'/about': linkTo('App', 'about')}))

Paths non matching the ones defined as argument of the StoryRouter will use the action addon as in the @nathancahill solution.

@gvaldambrini that seems like a great solution. Any plans to publish it as a package to npm? It seems like one that many of us would find useful. I certainly would.

@travi thanks. I'll create a storybook addon for that and I'll let you know when it will be ready & released (hopefully soon).

@travi the package for the decorator is now available on npm. Of course any feedback is welcome.

Was this page helpful?
0 / 5 - 0 ratings