Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I please trigger the route load before the page change using react-router? #203

Closed
kopax opened this issue Jan 15, 2019 · 3 comments

Comments

@kopax
Copy link

kopax commented Jan 15, 2019

Hi,

I would like to avoid the blinking when using a chunk page with loadable. We don't want to use a loader, we want to use a top navbar navigation loading indicator.

This is why we don't want to use <Link /> and don't want to use a fallback component while loading the chunk.

My component look like:

import loadable from '@loadable/component'
export default loadable(() => import(/* webpackPrefetch: true */ './index'));

How can I trigger the preloading? This is what I have tried so far

import React from 'react';
import PropTypes from 'prop-types';
import omit from 'lodash.omit';
import withRouter from 'react-router-dom/withRouter';
import A from '@bootstrap-styled/v4/lib/A';
import makeRoutes from '@yeutech-lab/react-router-dom-utils/lib/makeRoutes';
import { AppContextConsumer } from '../../AppContext';
import Sol from '../../containers/SolutionsPage/Loadable.js';

class Link extends React.Component {
  static propTypes = {
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  };

  onClick = (e, routes) => {
    e.preventDefault();
    e.stopPropagation();
    const { history, to } = this.props;
    const Routes = makeRoutes(routes);
    const selected = Routes.filter((route) => route.props.path === to)[0];
    console.log(selected.props.component.remder());
    history.push(to);
  };

  render() {
    const { to, ...rest } = omit(this.props, [
      'match',
      'location',
      'history',
      'onClick',
      'staticContext'
    ]);
    return (
      <AppContextConsumer>
        {({ routes }) => <A {...rest} onClick={(e) => this.onClick(e, routes)} href={to} />}
      </AppContextConsumer>
    );
  }
}

export default withRouter(Link);
@gregberge
Copy link
Owner

Hello @kopax, so to resume your problem, you want to be able to know if a component is loading? You can do it with fallback and context, an example.

@kopax
Copy link
Author

kopax commented Jan 17, 2019

@neoziro, I want to trigger action before the page actually change, this is how I have written my Link component in order to do that:

import React from 'react';
import PropTypes from 'prop-types';
import withRouter from 'react-router-dom/withRouter';
import makeRoutes from './makeRoutes';

/**
 * @name Link
 * @description
 *
 * This link component can preload a [react-loadable](https://github.com/jamiebuilds/react-loadable#loadablecomponentpreload) chunk on **mouseOver**
 * It can also block the page change before the chunk is loaded,
 * This give control on how the page change should work.
 *
 * @example
 *
 * Usually, you would create a `<Link />` component in your application `src/components/Link/index.js` as follow:
 *
 * ```javascript
 * export default (
 *   <Link
 *    tag={A}
 *    waitChunk={true}
 *    // You can pass routes as props
 *    routes={[{ name: 'Home', component: Home, path: '/' }]}
 *    // OR a ContextConsumer that own it
 *    ContextConsumer={AppContextConsumer}
 *    onClick={actionStartLoadingIndicator}
 *    onPreload={() => console.log(`
 *      chunk load in the background on mouseover or
 *      when user click if waitChunk is true
 *    `)}
 *    onLoaded={actionStopLoadingIndicator}
 *   />
 * );
 * ```
 *
 */
class Link extends React.Component {
  static propTypes = {
    /** the path the link will go to */
    to: PropTypes.string.isRequired,
    /** pass the component to be used for rendering */
    tag: PropTypes.any,
    /**
     * Avoiding Flash Of Loading Component
     * Sometimes components load really quickly (<200ms) and the loading screen only quickly flashes on the screen.
     * A number of user studies have proven that this causes users to perceive things taking longer than they really have.
     * If you don't show anything, users perceive it as being faster.
     * Useful if you use with waitChunk, To take over the delay of react-loadable
     * When delay is a function, it's return must be the number value of the delay
     *
     * See https://github.com/jamiebuilds/react-loadable#avoiding-flash-of-loading-component
     */
    delay: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.func,
    ]),
    /** define if prel oading of chunks should happen */
    preload: PropTypes.bool,
    /** event when click */
    onClick: PropTypes.func,
    /** event when page has changed */
    onPageChange: PropTypes.func,
    /** event when preloading start (react-loadable) */
    onPreload: PropTypes.func,
    /** event when preloading stop  (react-loadable) */
    onLoaded: PropTypes.func,
    /** event when mouse fly over */
    onMouseOver: PropTypes.func,
    /** event fired just before the page change */
    onBeforePageChange: PropTypes.func,
    /** define if react-router should change the page before or after the chunk is loaded */
    waitChunk: PropTypes.bool,
    /** the route list of the application, it supports childRoutes */
    routes: PropTypes.array,
    /**
     * A context consumer that will provide routes from it's context,
     * It also supports childRoutes.
     */
    ContextConsumer: PropTypes.any,
    /** @ignore */
    match: PropTypes.object.isRequired,
    /** @ignore */
    location: PropTypes.object.isRequired,
    /** @ignore */
    history: PropTypes.object.isRequired,
  };

  static defaultProps = {
    tag: 'a',
    delay: 0,
    waitChunk: false,
    preload: true,
    onClick: null,
    onBeforePageChange: null,
    onPageChange: null,
    onPreload: null,
    onLoaded: null,
    onMouseOver: null,
    routes: [],
    ContextConsumer: null,
  };

  componentWillMount() {
    const { routes, ContextConsumer } = this.props;
    if (routes.length && ContextConsumer && process.env.NODE_ENV !== 'production') {
      // eslint-disable-next-line no-console
      console.warn(
        'You passed routes and ContextConsumer props. You must use only one, ContextConsumer will be used and routes will be ignored.'
      );
    }
  }

  getComponent(path, routes) {
    return makeRoutes(routes)
      .filter((route) => route.props.path === path)[0]
      .props.component;
  }

  onMouseOver = (e, routes) => {
    e.preventDefault();
    e.stopPropagation();
    const {
      to,
      onMouseOver,
      onPreload,
      onLoaded,
      preload,
    } = this.props;
    const component = this.getComponent(to, routes);

    if (onMouseOver) {
      onMouseOver();
    }
    if (preload && component.preload) {
      if (onPreload) {
        onPreload(e);
      }
      component.preload().then(() => {
        if (onLoaded) {
          onLoaded(e);
        }
      });
    }
  };

  onClick = (e, routes) => {
    e.preventDefault();
    e.stopPropagation();
    const {
      to,
      preload,
      onClick,
      onPreload,
      onLoaded,
      waitChunk,
    } = this.props;
    const component = this.getComponent(to, routes);
    if (onClick) {
      onClick(e);
    }
    if (preload && waitChunk && component.preload) {
      if (onPreload) {
        onPreload(e);
      }
      component.preload().then(() => {
        if (onLoaded) {
          onLoaded(e);
        }
        this.changePage(e, to);
      });
      return;
    }
    this.changePage(e, to);
  };

  changePage = (e, path) => {
    const {
      history,
      delay,
      onPageChange,
      onBeforePageChange,
    } = this.props;
    const timeout = delay === 0 ? (cb) => cb() : setTimeout;
    const ms = typeof delay === 'function' ? delay() : delay;
    if (onBeforePageChange) {
      onBeforePageChange(e);
    }
    timeout(() => {
      if (onPageChange) {
        onPageChange(e);
      }
      history.push(path);
    }, ms);
  };

  render() {
    const {
      tag: Tag,
      to,
      routes,
      ContextConsumer,
      // unused below
      delay,
      preload,
      match,
      location,
      history,
      onClick,
      onPreload,
      onPageChange,
      onLoaded,
      onMouseOver,
      staticContext, // eslint-disable-line react/prop-types
      waitChunk,
      onBeforePageChange,
      ...rest
    } = this.props;
    return ContextConsumer ? (
      <ContextConsumer>
        {({ routes: contextRoutes }) => (
          <Tag
            {...rest}
            onClick={(e) => this.onClick(e, contextRoutes)}
            onFocus={(e) => this.onMouseOver(e, contextRoutes)}
            onMouseOver={(e) => this.onMouseOver(e, contextRoutes)}
            href={to}
            {...rest}
          />
        )}
      </ContextConsumer>
    ) : (
      <Tag
        {...rest}
        onClick={(e) => this.onClick(e, routes)}
        onFocus={(e) => this.onMouseOver(e, routes)}
        onMouseOver={(e) => this.onMouseOver(e, routes)}
        href={to}
        {...rest}
      />
    );
  }
}

export default withRouter(Link);

I have not found another way to do that, and I had to use react-loadable which allow me to trigger the chunk without actually changing the page.

@gregberge
Copy link
Owner

It will be solved by #196, I will implement it soon. Stay tuned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants