Git Product home page Git Product logo

redux-saga-router's Introduction

Redux Saga Router

Travis branch npm

A router for Redux Saga

Redux Saga Router gives you a saga for handling clientside routes in your Redux Saga application. This affords you a perfect way to manage side effects or dispatch Redux actions in response to route changes.

Table of Contents

Install

Yarn or npm.

yarn add redux-saga-router
npm install --save redux-saga-router

Usage

Redux Saga Router comes equipped with a router saga and two history strategies, createBrowserHistory and createHashHistory.

The router saga expects a history object and a routes object with key-value pairs of route paths to other sagas. It also takes an optional third argument with additional options.

To create a history object, you can use createBrowserHistory or createHashHistory. createBrowserHistory uses HTML5 pushState while createHashHistory uses (you guessed it) hashes, which is perfect for older browsers. These two history creation functions in fact come from the history library.

import { call, fork, put } from 'redux-saga';
import { router, createBrowserHistory } from 'redux-saga-router';

const history = createBrowserHistory();

const routes = {
  '/users': function* usersSaga() {
    const users = yield call(fetchUsers);
    yield put(setUsers(users));
  },

  '/users/:id': function* userSaga({ id }) {
    const user = yield call(fetchUser, id);
    yield put(setCurrentUser(user));
  },
};

function* mainSaga() {
  const data = yield call(fetchInitialData);

  yield put(ready(data));

  // The recommended way is to `fork` the router, but you can delegate with
  // yield* too
  yield fork(router, history, routes);
}

Behavior

Redux Saga Router will spawn the first matching route saga. When the location changes, the current running saga will be cancelled. As such, you might want to clean up your saga in that event.

If you wish to avoid your saga's being cancelled, you can spawn a sub saga in your route saga like the following:

const routes = {
  '/': function* homeSaga() {
    yield spawn(subSaga);
  },
};

In the event of an unhandled error occurring in one of your sagas, the error will stop the running saga and will not propagate to the router. That means that your application will continue to function when you hit other routes. That also means you should ensure you handle any potential errors that could occur in your route sagas.

Routes

Routes may be expressed as either an object or an array with the main difference being that the array form preserves order and, therefore, the precedence of routes.

const objectFormRoutes = {
  '/foo': fooHandler,
  '/bar': barHandler,
};

const arrayFormRoutes = [
  { pattern: '/foo', handler: fooHandler },
  { pattern: '/bar', handler: barHandler },
];

Exact Matching

This route will only match /foo exactly.

const routes = {
  '/foo': saga,
};

Path Parameters

You can capture dynamic path parameters by prepending them with the : symbol. The name you use will be assigned to a property of the same name on a parameters object that is passed into your route saga.

const routes = {
  // Capture the user id with `:id` into an `id` property of the parameters
  // object that is passed into `userSaga`.
  '/users/:id': function* userSaga({ id }) {
    const user = yield call(fetchUser, id);
    yield put(setCurrentUser(user));
  },

  // You can capture multiple dynamic path parameters too.
  '/dogs/:id/friends/:friendId': function* dogSaga({ id, friendId }) {
    // ...
  },
};

If you specify a dynamic path parameter, then it will be required. This route will match /bar/42 but NOT /bar.

const routes = {
  '/bar/:id': saga,
};

Optional Named Parameters

However, you can make a path parameter optional, by ending it with ?.

This route will match /bar/42 AND /bar.

const routes = {
  '/bar/:id?': saga,
};

Using a period before an optional parameter can be optional too.

This route will match /bar/LICENSE and /bar/README.md.

const routes = {
  '/bar/:fname.:ext?': saga,
};

Wildcard

You can use * as a wildcard to match many routes.

This route would match /bar and /bar/baz/foo.

const routes = {
  '/bar/*': saga,
};

Route Precedence

Sometimes you want some routes to take precedence over others. For example, consider a /users/invite route and a /users/:id route. JavaScript objects don't guarantee order, so the /users/:id route could take precedence and match /users/invite. So, the newUser handler would never run.

// Can't guarantee precedence with an object
const routes = {
  '/users/invite': inviteUser,
  '/users/:id': newUser,
};

To fix this problem, you can define routes with an array of route objects like so.

const routes = [
  { pattern: '/users/invite', handler: inviteUser },
  { pattern: '/users/:id', handler: newUser },
];

The array form will register routes in the order you provide, ensuring precedence.

Options

As mentioned earlier, the router saga may also take a third argument, an optional options object, which allows you to specify additional behaviour as described below:

Key Description
matchAll If set to true, it allows all matching routes to run instead of the first matching route.
beforeRouteChange Set to a saga to run any time location changes. This is useful for dispatching a cleanup action before route changes.
const options = {
  matchAll: true,

  *beforeRouteChange() {
    yield put(clearNotifications());
  },
};

function* mainSaga() {
  yield fork(router, history, routes, options);
}

Navigation

Hash History

If you use hash history, then navigation will work right out of the box.

import { router, createHashHistory } from 'redux-saga-router';

const history = createHashHistory();

const routes = {
  // ...
};

function* mainSaga() {
  const data = yield call(fetchInitialData);

  yield put(ready(data));

  yield fork(router, history, routes);
}
<nav>
  <ul>
    <li><a href="#/users">Users</a></li>
    <li><a href="#/users/1">A Specific User</a></li>
  </ul>
</nav>

Browser History

Browser history depends on pushState changes, so you'll need a method for making anchor tags change history state instead of actually exhibiting their default behavior. Also, if you're building a single-page application, your server will need to support your client side routes to ensure your app loads properly.

import { router, createBrowserHistory } from 'redux-saga-router';

const history = createBrowserHistory();

// This is a naive example, so you might want something more robust
document.addEventListener('click', (e) => {
  const el = e.target;

  if (el.tagName === 'A') {
    e.preventDefault();
    history.push(el.pathname);
  }
});

const routes = {
  // ...
};

function* mainSaga() {
  // ...
}

Browser History with React

If you're using React in your application, then Redux Saga Router does export a higher-order component (HOC) that allows you to abstract away dealing with pushState manually. You can import the createLink HOC from redux-saga-router/react to create a Link component similar to what's available in React Router. Just pass in your history object to the createLink function to create the Link component. You'll probably want a separate file in your application for exporting your history object and your Link component.

If you are also using React Router, you can use the Link component that is shipped with React Router.

// history.js

import { createLink } from 'redux-saga-router/react'

// Without React Router v4:
import { createBrowserHistory } from 'redux-saga-router';

// With the history npm package:
import createBrowserHistory from 'history/createBrowserHistory';

const history = createBrowserHistory();

export const Link = createLink(history);
export { history };
// saga.js

import { router } from 'redux-saga-router';
import { history } from './history';

const routes = {
  // ...
};

function* mainSaga() {
  const data = yield call(fetchInitialData);

  yield put(ready(data));

  yield fork(router, history, routes);
}
// App.js

import React from 'react';
import { Link } from './history';

export default function App() {
  return (
    <nav>
      <ul>
        <li><Link to="/users">Users</Link></li>
        <li><Link to="/users/1">A Specific User</Link></li>
      </ul>
    </nav>
  );
}

React Router

Redux Saga Router can also work in tandem with React Router (v2, v3, and v4)! Instead of using one of Redux Saga Router's history creation functions, just use your history object from React Router (v2, v3) or use the history creation functions provided by the history npm package (v4).

// saga.js

import { router } from 'redux-saga-router';

// React Router v2 and v3:
import { browserHistory as history } from 'react-router';

// React Router v4:
import createBrowserHistory from 'history/createBrowserHistory';
const history = createBrowserHistory();

const routes = {
  // ...
};

export default function* mainSaga() {
  const data = yield call(fetchInitialData);

  yield put(ready(data));

  yield fork(router, history, routes);
}
// App.js

import React from 'react';
import { Link } from 'react-router';

export default function App({ children }) {
  return (
    <div>
      <nav>
        <ul>
          <li><Link to="/users">Users</Link></li>
          <li><Link to="/users/1">A Specific User</Link></li>
        </ul>
      </nav>

      <div>
        {children}
      </div>
    </div>
  );
}
import React from 'react';
import { render } from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import App from './App';
import Users from './Users';
import User from './User';
import mainSaga from './saga';

// React Router v2 and v3:
import { Router, Route, browserHistory as history } from 'react-router';

// React Router v4:
import createBrowserHistory from 'history/createBrowserHistory';
import { Router, Route } from 'react-router';
const history = createBrowserHistory();

function reducer() {
  return {};
}

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(mainSaga);

render((
  <Router history={history}>
    <Route path="/" component={App}>
      <Route path="/users" component={Users} />
      <Route path="/users/:id" component={User} />
    </Route>
  </Router>
), document.getElementById('main'));

redux-saga-router's People

Contributors

jfairbank avatar paulcoyle avatar teotn avatar victorchabbert avatar visusnet 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  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  avatar  avatar  avatar  avatar  avatar  avatar

redux-saga-router's Issues

Error Messages

Hi

I tried to use your package but i am facing following errors

Please userequire("history").createBrowserHistoryinstead ofrequire("history/createBrowserHistory"). Support for the latter will be removed in the next major release.

Uncaught TypeError: Cannot read property 'call' of undefined

I am not sure if its relevant, but can you point out how do I fix them
Thanks

No guaranteed way to register routes in order

It would be nice to be able to pass in routes as an Array instead of an Object so you know the order in which routes are registered with ruta3 is stable. Either that, or allow passing of a route matching object through options. Thoughts?

Improve Docs

Along with examples, the docs in the README need to be more robust and highlight the API options better.

Uncaught TypeError: Unable to read the 'string' property undefined in createLink (createLink.js: 56)

We use redux-saga-router in the project.

But, as soon as I'm going to use the links:

import PropTypes from 'prop-types' // to react this is now a separate library
export const Link = createLink (history);
...

  • Users
  • Specific User

    The library redux-saga-router begins to swear:
    Uncaught TypeError: Unable to read the 'string' property undefined in createLink (createLink.js: 56)

    This is because the developers first introduced the PropTypes into the reaction, and from 15.3.0 they took it to a separate library. And redux-saga-router does not know about this and is trying to perform a check:

    Link.propTypes = {
    to: _react.PropTypes.string.isRequired,
    className: _react.PropTypes.string,
    children: _react.PropTypes.any
    };};

    How to solve the problem without giving up the library?

  • Access Wildcard Value?

    Currently using window.location.href and then splitting on ('/') to get the value of '' in /:my/:path/

    Is there a better way to do this?

    beforeRouteChange always reports history.action as "POP"

    See screenshot: https://puu.sh/Awx72/96dad7e153.png

    There are two sets of logging lines: one is in the beforeRouteChange method passed in as options to redux-saga-router, the other is using react-router-redux to listen to history updates. The latter doesn't post an initial log line but beforeRouteChange correctly logs a POP action.

    However, for some reason, beforeRouteChange continues to always report a routing action as POP when it should (I think) be PUSH for subsequent routing actions.

    Add the option to cancel sagas on location change and to choose buffer size and behavior

    Like the title says, I think it would be useful to have an option to cancel sagas on a location change and/or to have control of the saga buffer globally and per route. This could probably be split into two issues. Thoughts ?

    About the cancellation problem, I've explored the source already and saw that you do not keep a reference of the previous value which would have helped the cancellation of the value if it's a saga. How would you go about that ?

    I'd like to help you figure it out and why not make my first PR !

    Cheers :)

    redux-saga effects not attached.

    Hi. i'm daniel.
    I have a question about it.

    I got a some issue When i use redux-saga-router in my projects.

    package dependencies version.

    • redux-saga: ^1.1.1
    • redux-saga-router: 2.2.0

    when i follow the example configuration in README.md

    i got a

    TypeError: Cannot read property 'call' of undefined
    var call = exports.call = _reduxSaga.effects.call;
    

    I thought that error using way for effects module are changed

    BEFORE:
    import {effects } from 'redux-saga';
    AFTER:
    import { call } from 'redux-saga/effects';
    

    That means redux-saga-router does not support redux-saga 1.1.1 version?

    if not support it, could i downgrade my saga version? or has other way for solving it?

    How detect leaving from current location

    Code below not work
    `const routes = {
    '/assets/:id/:tab?': function*({ id }) {

        try {
            yield* fetchAssetsItemSaga(id);
    
        } finally {
            if (yield cancelled()) {
                console.log('leave from', id);
            }
        }
    }
    

    };`

    [Question] How do we trigger a route change component to render using react-router and redux-saga-router

    Should the saga router live next to the react-router ?

    I don't see any way to define my previous router in sagas :

    export default function createRoutes(store) {
      // Create reusable async injectors using getAsyncInjectors factory
      const { injectReducer, injectSagas } = getAsyncInjectors(store);
    
      injectSagas(appSagas);
      injectSagas([routeSagas]);
    
      return [
        {
          path: pages.pageLogin.path,
          name: pages.pageLogin.name,
          onEnter: checkAuth(store),
          getComponent(nextState, cb) {
            const importModules = Promise.all([
              import('containers/LoginPage/reducer'),
              import('containers/LoginPage/sagas'),
              import('containers/LoginPage'),
            ]);
    
            const renderRoute = loadModule(cb);
            importModules.then(([reducer, saga, component]) => {
              // here we load asynchronously one saga and one reducer
              injectReducer(STORE_LOGIN_MAIN, reducer.default);
              injectSagas(saga.default);
              renderRoute(component);
            });
          },
        },
        {
          path: pages.pageDashboard.path,
          name: pages.pageDashboard.name,
          onEnter: checkAuth(store),
          getComponent(nextState, cb) {
            const importModules = Promise.all([
              import('containers/DashboardPage/reducers/index'),
              import('containers/DashboardPage/sagas/index'),
              import('containers/DashboardPage'),
            ]);
    
            const renderRoute = loadModule(cb);
            importModules.then(([reducers, sagas, component]) => {
              // here we load asynchronously an array of sagas and reducers
              reducers.default.forEach((reducer) => injectReducer(reducer.name, reducer.reducer));
              sagas.default.forEach((saga) => injectSagas(saga));
              renderRoute(component);
            });
    
            importModules.catch(errorLoading);
          },
        },
        {
          path: pages.pageTesting.path,
          name: pages.pageTesting.name,
          onEnter: checkAuth(store),
          getComponent(nextState, cb) {
            import('containers/TestingPage')
              .then(loadModule(cb))
              .catch(errorLoading);
          },
        },
        {
          path: pages.pageTestingagd.path,
          name: pages.pageTestingagd.name,
          getComponent(location, cb) {
            import('containers/TestingagdPage')
              .then(loadModule(cb))
              .catch(errorLoading);
          },
        },
        {
          path: '*',
          name: 'notfound',
          onEnter: checkAuth(store),
          getComponent(nextState, cb) {
            import('containers/NotFoundPage')
              .then(loadModule(cb))
              .catch(errorLoading);
          },
        },
      ];
    }

    I need to call a services when arriving on /dashboard, this is my sagas.js, this work but break my previous route :

    import { router, } from 'redux-saga-router';
    import { browserHistory } from 'react-router';
    
    import { call, put } from 'redux-saga/effects';
    
    import auth from 'services/auth';
    
    const routes = {
      // Method syntax
      *'/dashboard'() {
        const services = yield call(auth.services);
        console.log(services);
        // yield put(setUsers(users));
      },
      //
      // // Or long form with function expression
      // '/users/:id': function* userSaga({ id }) {
      //   const user = yield call(fetchUser, id);
      //   yield put(setCurrentUser(user));
      // },
    };
    
    function* fetchInitialData(){
    
    }
    
    export default function* mainSaga() {
      // const data = yield call(fetchInitialData);
      //
      // yield put(ready(data));
      const start = new Date();
      console.log("timeout start", start.toISOString());
      console.log("start", new Date().getTime() - start.getTime());
      const toto = yield (call(setTimeout, 1000, () => new String('toto')));
      console.log("timeout end", new Date().getTime() - start.getTime());
      yield* router(browserHistory, routes);
    }

    I don't see any code example online, would be nice to get some help here. :)

    Sample Project?

    Does anyone have a sample project for any of the supported APIs?

    I would like to use this package, but I'm new to Saga and I'm not even sure how to wire it in to the app. Hash history works "out of the box" but the code sample to integrate mainSaga into App.js is only for browser history. Is the integration similar?

    There are no repos that I can find that use this package at all, and with no working sample repos to learn from, it makes it quite difficult to get started with this router.

    **The biggest question I have is:

    I see the routes are defined, and result in handler functions, but exactly where do the components get defined in the route? eg. How do I render a component with this router?

    The only mention of how to wire components are in "not out of the box" answers. How is it intended to render components "right out of the box"?

    **

    I don't want to use react-router, I've found it's a pita to test with, and my next option would (https://github.com/supasate/connected-react-router), which has saga support, but this seems like a more "saga native" approach.

    This router looks to be preferable to me, but I'm really taken aback by the lack of docs. I'm sure you have some samples that you have tested with just to make sure it works?

    Feature request: afterRouteChange and/or onRouteChange options

    I frequently run into situations where it would be helpful to know that a route has just changed or is in the process of changing.

    Think of something like React's lifecycle methods.

    As an example, it might be handy to know that a user has just navigated between two given routes in order to refresh the data held in redux state.

    example:

    const options = {
      *onRouteChange({ routeParams, nextRouteParams }) {
        if ( routeParams.section === 'user' && nextRouteParams.subsection === 'posts' ) {
              // do something with the params
        }
      }
    }
    

    Add examples

    An examples folder with demos to show different use cases would be helpful

    Is it possible to use this library with SSR?

    Any thoughts or examples how to use this library with SSR ?

    I guess I can use this library only on the client and implement some additional functionality based on the same router config for server. But in this case it could be useful to export from the library functionality for route matching.

    Like the approach this library suggests. Thanks.

    [Feature Request] Fall through pattern matching

    Hi! I've noticed that ruta3 library which redux-saga-router is based on supports matching multiple patterns to one location. It can be useful when we want to have some general saga to be applied alongside detailed ones. For instance:

    const routes = {
      '/profile/:id/': getProfileData,
      '/profile/:id/friends/': getProfileFriends,
      '/profile/:id/projects/': getProfileProjects,
    }
    

    It is easy to imagine, that on every subpage of /profile/:id we'd like to have some basic data fetched when visiting for the first time (e.g. username, photo, etc.) and also some detailed data.

    I'd suggest adding such feature as an optional setting - and I am issuing a pull request for that.
    BTW My pull request also includes updated README.md with detailed information on what patterns are available.

    ...effects] has been deprecated

    I get a warning in Chrome:

    [...effects] has been deprecated in favor of all([...effects]), please update your code
    

    just by adding the router to the root saga

    function* rootSaga() {
      yield all([
        fork(router, history, routes)
      ])
    }
    

    Will the project be maintained?

    I've noticed that last commit is from more than a year ago (11/2/2018) and that PRs for updating redux-saga to v1.x.x are pending without response.
    @jfairbank Do you plan to take care of the library in the near future?

    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.