Giter Site home page Giter Site logo

react-router-redial's Introduction

react-router-redial Travis npm package Coveralls

Simple integration of redial for React Router

$ npm install --save react-router-redial redial

Why?

Data fetching is an important part of applications and redial is a great way to manage this in React when using React Router. This project aims to provide a great way to use redial together with React Router with a simple but yet powerful API.

Additionally it offers an alternative way to manage data for components without the need of flux. This means that you can start creating an application without for instance Redux and when you see the need for it you can easily update your application gradually.

react-router-redial has been inspired by AsyncProps and can be seen as an alternative to it.

Works with universal applications that run on the server and the client as well as client only applications.

Works with IE9 if a Promise polyfill is provided.

Difference from redial

  • Simple integration with React Router
  • Managing client side data loading with route transition support
  • Powerful API to control hooks on both client and server
  • Alternative way to pass data to components without the need of Redux

Difference from AsyncProps

  • Uses redial hooks to manage data loading
  • Possible to easily transition to Flux/Redux

Lifecycle hooks

You use @provideHooks as would normally when using redial. One difference is that react-router-redial will provide some default locals to the hooks.

import { provideHooks } from 'redial';

import React, { Component } from 'react';
import { getSomething } from 'actions/things';

@provideHooks({
  fetch: ({ dispatch, params: { id } }) => dispatch(getSomething(id)),
  defer: ({ setProps, getProps, force }) => {
    const { data } = getProps();
    if(!data || force) {
      // Will be available as this.props.data on the component
      setProps({ data: 'My important data' })
    }
  }
})
class MyRouteHandler extends Component {
  render() {
    return <div>{ this.props.data }</div>;
  }
}

Default locals provided to the hooks

setProps          Makes it possible to set things that should be available to the component as props, should be an object
getProps          Makes it possible to get things that has been defined for the component, can be used for bailout
force             If the provideHooks has been invoked using the reload function
params            Route params from React Router
location          Location object from React Router
routeProps        Custom defined properties that has been defined on the route components
isAborted         Function that returns if the hooks has been aborted, can be used to ignore the result

Default props available to decorated components

loading                 Will be true when beforeTransition hooks are not yet completed
afterTransitionLoading  Will be true when afterTransition hooks are not yet completed
reload                  Function that can be invoked to re-trigger the hooks for the current component
abort                   Function that can be invoked to abort current running hooks

Additionally components will have access to properties that has been set using setProps.

Client API

The custom redial router middleware useRedial makes it easy to add support for redial on the client side using the render property from Router in React Router. It provides the following properties as a way to configure how the data loading should behave.

locals                     Extra locals that should be provided to the hooks other than the default ones
beforeTransition           Hooks that should be completed before a route transition is completed
afterTransition            Hooks that are not needed before making a route transition
parallel                   If set to true the afterTransition hooks will run in parallel with the beforeTransition ones
initialLoading             Component should be shown on initial client load, useful if server rendering is not used
onStarted(force)           Invoked when a route transition has been detected and when redial hooks will be invoked
onError(error, metaData)   Invoked when an error happens, see below for more info
onAborted(becauseError)    Invoked if it was prematurely aborted through manual interaction or an error
onCompleted(type)          Invoked if everything was completed successfully, with type being either "beforeTransition" or "afterTransition"

onError(error, metaData)

metaData

abort()            Function that can be used to abort current loading    
beforeTransition   If the error originated from a beforeTransition hook or not
reason             The reason for the error, can be either a "location-changed", "aborted" or "other"
router             React Router instance https://github.com/ReactTraining/react-router/blob/master/docs/API.md#contextrouter

Example

We can use onError to add handling for errors in our application. The example below shows how we can make the client either reload the page or transition back to the previous page on an error.

const forcePageReloadOnError = true;
const goBackOnError = false;

// Function that can be used as a setting for useRedial
function onError(err, { abort, beforeTransition, reason, router }) {
  if (process.env.NODE_ENV !== 'production') {
    console.error(reason, err);
  }

  // We only what to do this if it was a beforeTransition hook that failed
  if (beforeTransition) {
    if (forcePageReloadOnError && reason === 'other') {
      window.location.reload();
    } else if (goBackOnError && reason !== 'location-changed') {
      router.goBack();
    }
    // Abort current loading automatically
    abort();
  }
}

Example

import { useRedial } from 'react-router-redial';
import { applyRouterMiddleware } from 'react-router';

<Router
  history={ browserHistory }
  routes={ routes }
  render={ applyRouterMiddleware(
    useRedial({
        locals,
        beforeTransition: ['fetch'],
        afterTransition: ['defer', 'done'],
        parallel: true,
        initialLoading: () => <div>Loading…</div>,
    })
  )}
/>

Server API

Instead of using trigger on the server side we will use a wrapper that provides additional functionally named triggerHooks. It takes an object as an argument that has the following properties that can be used to configure how the data loading should behave.

components        The components that should be scanned for hooks, will default to renderProps.components
renderProps       The renderProps argument that match from React Router has in the callback
hooks             The hooks that should run on the server
locals            Additional locals that should be provided over the default, useful for Redux integration for example

triggerHooks will return a Promise that will resolve when all the hooks has been completed. It will be resolved with an object that contains the following properties.

redialMap         This should be used together with RedialContext on the server side
redialProps       This is for passing the props that has been defined with setProps to the client, expected to be on window.__REDIAL_PROPS__

Example

import { triggerHooks } from 'react-router-redial';

const locals = {
  some: 'data',
  more: 'stuff'
};

triggerHooks({
  components,
  renderProps,
  hooks,
  locals
}).then(({ redialMap, redialProps }) => render(redialMap, redialProps));

Hooks

react-router-redial provides a simple way to define in what order certain hooks should run and if they can run in parallel. The same syntax is used for both hooks when used on the server with triggerHooks and beforeTransition + afterTransition on the client with RedialContext. The hooks are expected to be an array the can contain either single hooks or arrays of hooks. Each individual element in the array will run in parallel and after it has been completed the next element will be managed. This means that you can run some hooks together and others after they have been completed. This is useful if you for instance want to run some hook that should have access to some data that other hooks before it should have defined.

Example

Let's look at an example to understand this a bit better. Say that we have the following hooks defined on the server:

{
  hooks: [ ['fetch', 'defer'], 'done' ]
}

This means that the fetch and defer hooks will run at the same time and after they have been completed the done hook will run and it will then have access to the potential data that they might have set using either setProps or with for example Redux.

Example

Client

import { useRedial } from 'react-router-redial';

import React from 'react';
import { render } from 'react-dom';
import { Router, browserHistory, applyRouterMiddleware } from 'react-router';
import { Provider } from 'react-redux';

// Your app's routes:
import routes from '../shared/routes';

// Render the app client-side to a given container element:
export default (container, store) => {
  // Define extra locals to be provided to all lifecycle hooks:
  const locals = store ? {
    dispatch: store.dispatch,
    getState: store.getState,
  } : {};

  let component = (
    <Router
      history={browserHistory}
      routes={routes}
      render={ applyRouterMiddleware(
        useRedial({
          locals,
          beforeTransition: ['fetch'],
          afterTransition: ['defer', 'done'],
          parallel: true,
          initialLoading: () => <div>Loading…</div>,
        })
      )}
    />
  );

  if (store) {
    component = (
      <Provider store={store}>
        {component}
      </Provider>
    );
  }

  // Render app to container element:
  render(component, container);
};

Server

import { triggerHooks, useRedial } from 'react-router-redial';

import React from 'react';
import { renderToString } from 'react-dom/server';
import { createMemoryHistory, match, applyRouterMiddleware } from 'react-router';
import { Provider } from 'react-redux';

// Your app's routes:
import routes from '../shared/routes';

// Render the app server-side for a given path:
export default (path, store) => new Promise((resolve, reject) => {
  // Set up history for router:
  const history = createMemoryHistory(path);

  // Match routes based on history object:
  match({ routes, history }, (error, redirectLocation, renderProps) => {

    // Define extra locals to be provided to all lifecycle hooks:
    const locals = store ? {
      dispatch: store.dispatch,
      getState: store.getState
    } : {};

    // Wait for async data fetching to complete, then render:
    triggerHooks({
      renderProps,
      locals,
      hooks: [ 'fetch', 'done' ]
    }).then(({ redialMap, redialProps }) => {
      const state = store ? store.getState() : null;
      // Use `applyRouterMiddleware` to create the `<RouterContext/>`,
      // as well as the `<RedialContext/>` and `<RedialContextContainer/>`
      // around each matched route. Pass in the `redialMap` to the middleware
      // to ensure we have access to it while rendering, and
      // pass the renderProps provided from `match`
      const component = applyRouterMiddleware(useRedial({ redialMap }))(renderProps);
      const html = store ? renderToString(
        <Provider store={store}>
          { component }
        </Provider>
      ) : renderToString(component);

      // Important that the redialProps are sent to the client
      // by serializing it and setting it on window.__REDIAL_PROPS__
      resolve({ html, state, redialProps });
    })
    .catch(reject);
  });
});

Contributors

react-router-redial's People

Contributors

dlmr avatar pakerstrand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

react-router-redial's Issues

Write tests

Write tests that verify the functionality.

Don't re-fetch in layouts what are not changes in route transition

First of all, thanks for this library. I'm using it in all my projects. I'm really appreciated.

I'm strictly following the rule "fetch the data in the layout, pages, where they are used". So, the global data is fetching on some main layout of the app, and specific data for the page is fetching on that page.

I see that when I define a fetch hook in global layout, it's re-fetching on every transition between the pages, that are included in this layout.

<Route component={GlobalLayout}> // fetch api/global
  <Route path="/page-a" component={PageA} /> // fetch api/page-a
  <Route path="/page-b" component={PageA} /> // fetch api/page-b
</Router>

According to this example, when I go from pageA to pageB, fetch for api/global is triggered again, but it was fetched previously.

Can you recommend a way to avoid this?

Rename blocking and defer to something easier to understand

I suggest we change some of the terminology used in the API to something easier to understand and reason about.

Old New
blocking beforeTransition
defer afterTransition
parallel runAfterTransitionImmediately

Thoughts about this change?

Support finer control of which hooks to reload()

We currently get reload() exposed for refreshing the fetched data which is pretty awesome.

In some cases it would be nice to be able to specify a subset of the hook data to refresh. This could look something like this, with an optional parameter hookNames for example.
reload(['fetch']); would only reload data for the fetch hook and leave the others as-is.

Dynamic routing validation

Hi! Thank you for this great library!

I'm looking for a solution for displaying 404 error page without a redirect. I think It will be great If we can abort a matched route and find next one.

Example:

<Router>
 <Route path="item/:id" component={ItemPage} />
 <Route path="*" component={NotFoundPage} />
</Router>

So, can we modify the library to do this?

Re-render issue

https://github.com/dlmr/react-router-redial/blob/master/src/RedialContainer.js#L28

Every time, container generate new abort and reload functions instances, so can`t skip re-render by using PureComponents.

For some reason, can`t fork a repo, so will paste code here:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import getRoutePath from './util/getRoutePath';

export default class RedialContainer extends Component {
  static displayName = 'RedialContainer';

  static propTypes = {
    children: PropTypes.element.isRequired,
    routerProps: PropTypes.object.isRequired,
  };

  static contextTypes = {
    redialContext: PropTypes.object.isRequired,
  };

  reload = () => {
    this.context.redialContext.reloadComponent(this.props.routerProps.route.component);
  }

  render() {
    const { routerProps, ...props } = this.props;
    const {
      abortLoading,
      loading,
      afterTransitionLoading,
      redialMap,
    } = this.context.redialContext;
    const mapKey = getRoutePath(routerProps.route, routerProps.routes, routerProps.key);
    const redialProps = redialMap.get(mapKey);

    return React.cloneElement(
      this.props.children,
      {
        ...props,
        ...redialProps,
        ...routerProps,
        loading,
        afterTransitionLoading,
        reload: this.reload,
        abort: abortLoading,
      }
    );
  }
}

Cannot find module 'redial'

I just installed it and it makes this error. Maybe it should be installed when we run npm install react-router-redial? I solve my problem by installing it manually.

Error: Cannot find module 'redial'
    at Function.Module._resolveFilename (module.js:455:15)
    at Function.Module._load (module.js:403:25)
    at Module.require (module.js:483:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/Mike/Desktop/react-webpack-startup/node_modules/react-router-redial/lib/triggerHooks.js:11:15)
    at Module._compile (module.js:556:32)
    at Module._extensions..js (module.js:565:10)
    at Object.require.extensions.(anonymous function) [as .js] (/Users/Mike/Desktop/react-webpack-startup/node_modules/babel-register/lib/node.js:152:7)
    at Module.load (module.js:473:32)
    at tryModuleLoad (module.js:432:12)

SSR Client still makes initial fetch

Hey There,

Before I implemented this library I had some code that would listen to the browserHistory and only trigger defer on page load, and fetch / defer for every route change thereon.

Does this happen the same way in this library, or am I going back to checking if the data is in the store on the client before dispatching the redux action?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.