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

Dynamic routing - connecting everything the right way (help wanted) #1452

Closed
denisflorkin opened this Issue Jan 13, 2017 · 10 comments

Comments

Projects
None yet
7 participants
@denisflorkin

denisflorkin commented Jan 13, 2017

Hello,

I'm working on integrating some dynamic routes, following the doc
I manage to have the route trigger the saga (it would seems I'm also having this issue but this is not the problem I'm having right now).

My issue is that instead of actually getting the value from :slug in my saga, I get 0 (Number)

The router has the right data and 'knows' the value :slug
I'm new to redux and sagas, I have no idea where this is coming from or how to go about diagnosing.

routes.js :

(...)
    }, {
      path: '/profile/:slug',
      name: 'userProfile',
      getComponent(nextState, cb) {
        console.log('loadign dynamic route profile/: ; nextState', nextState)
        const importModules = Promise.all([
          System.import('containers/ProfilePage/reducer'),
          System.import('containers/ProfilePage/sagas'),
          System.import('containers/ProfilePage'),
        ]);

        const renderRoute = loadModule(cb);

        importModules.then(([reducer, sagas, component]) => {
          injectReducer('profilePage', reducer.default);
          injectSagas(sagas.default);
          renderRoute(component);
        });

        importModules.catch(errorLoading);
      },
    }, {
(...)

sagas.js

export function* getUserData(slug) {
  /** this is my problem : */
  console.log(' *getUserData slug', slug) // outputs:  0 

  const requestURL =
    `https://localhost/projects/neoapi/${slug}/GET.json`

  const userprofile = yield call(request, requestURL);
  console.log('userprofile', userprofile)
  if (!userprofile.err) {
    yield put(getUserProfileDataSuccess(userprofile));
  } else {
    yield put(getUserProfileDataFail(userprofile.err));
  }
}

/**
 * Watches for LOAD_PROFILE actions and calls getUserProfileData when one comes in.
 */
export function* getUserDataWatcher() {
  const { slug } = yield take(LOAD_PROFILE);
  console.log('getUserDataWatcher(slug)\'s slug', slug)
  yield call(getUserProfileData, slug);
}

/**
 * Root saga manages watcher lifecycle
 */
export function* userData() {
  // Fork watcher so we can continue execution
  const watcher = yield fork(getUserDataWatcher);

  // Suspend execution until location changes
  yield take(LOCATION_CHANGE);
  yield cancel(watcher);
}

// Bootstrap sagas
export default [
  getUserData,
];

Here is a screenshot of the browser console output:
screen shot 2017-01-13 at 1 01 39 pm

Any help would be greatly appreciated

@kylesavant

This comment has been minimized.

Show comment
Hide comment
@kylesavant

kylesavant Jan 13, 2017

This is how I do it with a slug called roomId:

actions.js
Make sure the action has an argument (roomId) and that it's passed on in the action

export function fetchRoom(roomId) {
  return {
    type: FETCH_ROOM,
    roomId,
  };
}

routes.js
Dispatch the action and inject the slug. Actions imported in importModules
store.dispatch(actions.fetchRoom(nextState.params.roomId));

sagas.js

export function* getRoom(action) {
  const roomId = action.roomId;
  let room = yield select(selectRoom(roomId));
  if (room) {
    yield put(roomLoaded(room)); // Update global room
  }
}

export function* showRoom(action) {
  console.log('show ', action.room)
}


export function* getRoomWatcher() {
  yield fork(takeLatest, FETCH_ROOM, getRoom);
}

export function* roomData() {
  const watcher = yield fork(getRoomWatcher);
}

// Bootstrap sagas
export default [
  roomData,
];

kylesavant commented Jan 13, 2017

This is how I do it with a slug called roomId:

actions.js
Make sure the action has an argument (roomId) and that it's passed on in the action

export function fetchRoom(roomId) {
  return {
    type: FETCH_ROOM,
    roomId,
  };
}

routes.js
Dispatch the action and inject the slug. Actions imported in importModules
store.dispatch(actions.fetchRoom(nextState.params.roomId));

sagas.js

export function* getRoom(action) {
  const roomId = action.roomId;
  let room = yield select(selectRoom(roomId));
  if (room) {
    yield put(roomLoaded(room)); // Update global room
  }
}

export function* showRoom(action) {
  console.log('show ', action.room)
}


export function* getRoomWatcher() {
  yield fork(takeLatest, FETCH_ROOM, getRoom);
}

export function* roomData() {
  const watcher = yield fork(getRoomWatcher);
}

// Bootstrap sagas
export default [
  roomData,
];

@jeremyadavis

This comment has been minimized.

Show comment
Hide comment
@jeremyadavis

jeremyadavis Jan 13, 2017

Contributor

I setup most of my sagas like @kylesavant did. Note that the takeLatest effect will pass on the arguments so it might seem like magic how the 'action' param makes it the getRoom method.

One thing to correct in your example would be to replace your default export in the saga to userData, not getUserData. Currently it is bypassing the watcher generators so it's correct that the slug is 0.

Also be sure that your LOAD_PROFILE action creator is dispatching the slug value so the getUserDataWatcher can grab it and pass it on to call what I think should be the getUserData generator, not getUserProfileData.

Contributor

jeremyadavis commented Jan 13, 2017

I setup most of my sagas like @kylesavant did. Note that the takeLatest effect will pass on the arguments so it might seem like magic how the 'action' param makes it the getRoom method.

One thing to correct in your example would be to replace your default export in the saga to userData, not getUserData. Currently it is bypassing the watcher generators so it's correct that the slug is 0.

Also be sure that your LOAD_PROFILE action creator is dispatching the slug value so the getUserDataWatcher can grab it and pass it on to call what I think should be the getUserData generator, not getUserProfileData.

@denisflorkin

This comment has been minimized.

Show comment
Hide comment
@denisflorkin

denisflorkin Jan 14, 2017

Hello @kylesavant, thanks for the example, it made things more clear for me. My action was ok, the problem was that I did not dispatch anything - this boilerplate is so awesome that I might have excepted to do it automagically... :)
An as @jeremyadavis noticed I was trying to call unexisting function 'getUserProfileData' and also not exporting the correct generator as default export.
This is working now.
Thanks for your help

I'm still a bit un clear on the saga canceling, shouldn't the saga for my 'profilepage' container/route be canceled on 'LOCATION_CHANGE' when it's not needed anymore ? Or is 'something' actually automagically taking care of this ?

I'll leave my updated code here for the next one it might help :

sagas.js :

/**
 * Gets the profile data of a user
 */
import { takeLatest } from 'redux-saga';
import {
  take,
  call,
  put,
  fork,
  cancel } from 'redux-saga/effects';

// import { LOCATION_CHANGE } from 'react-router-redux';

import {
  LOAD_PROFILE,
} from './constants';

import {
  getUserProfileData,
  getUserProfileDataSuccess,
  getUserProfileDataFail,
} from './actions';

import request from 'utils/request';


export function* getUserData(action) {
  const requestURL =
    `https://localhost/projects/neoapi/user/${action.slug}/GET.json`

  const userprofile = yield call(request, requestURL);
  if (!userprofile.err) {
    yield put(getUserProfileDataSuccess(userprofile));
  } else {
    yield put(getUserProfileDataFail(userprofile.err));
  }
}

/**
 * Watches for LOAD_PROFILE actions and calls getUserData when one comes in.
 * By using `takeLatest` only the result of the latest API call is applied.
 */
export function* getUserDataWatcher() {
  yield fork(takeLatest, LOAD_PROFILE, getUserData);
}

/**
 * Root saga manages watcher lifecycle
 */
export function* userData() {
  // Fork watcher so we can continue execution
  const watcher = yield fork(getUserDataWatcher);
}

// Bootstrap sagas
export default [
  userData,
];

actions.js :

(..)
export function getUserProfileData(slug) {
  return {
    type: LOAD_PROFILE,
    slug,
  };
}
(..)

routes.js

(..)
}, {
      path: '/profile/:slug',
      name: 'userProfile',
      getComponent(nextState, cb) {
        const importModules = Promise.all([
          System.import('containers/ProfilePage/actions'),
          System.import('containers/ProfilePage/reducer'),
          System.import('containers/ProfilePage/sagas'),
          System.import('containers/ProfilePage'),
        ]);

        const renderRoute = loadModule(cb);

        importModules.then(([actions, reducer, sagas, component]) => {
          injectReducer('profilePage', reducer.default);
          injectSagas(sagas.default);
          renderRoute(component);
          store.dispatch(actions.getUserProfileData(nextState.params.slug))
        });

        importModules.catch(errorLoading);
      },
    }, {
(..)

denisflorkin commented Jan 14, 2017

Hello @kylesavant, thanks for the example, it made things more clear for me. My action was ok, the problem was that I did not dispatch anything - this boilerplate is so awesome that I might have excepted to do it automagically... :)
An as @jeremyadavis noticed I was trying to call unexisting function 'getUserProfileData' and also not exporting the correct generator as default export.
This is working now.
Thanks for your help

I'm still a bit un clear on the saga canceling, shouldn't the saga for my 'profilepage' container/route be canceled on 'LOCATION_CHANGE' when it's not needed anymore ? Or is 'something' actually automagically taking care of this ?

I'll leave my updated code here for the next one it might help :

sagas.js :

/**
 * Gets the profile data of a user
 */
import { takeLatest } from 'redux-saga';
import {
  take,
  call,
  put,
  fork,
  cancel } from 'redux-saga/effects';

// import { LOCATION_CHANGE } from 'react-router-redux';

import {
  LOAD_PROFILE,
} from './constants';

import {
  getUserProfileData,
  getUserProfileDataSuccess,
  getUserProfileDataFail,
} from './actions';

import request from 'utils/request';


export function* getUserData(action) {
  const requestURL =
    `https://localhost/projects/neoapi/user/${action.slug}/GET.json`

  const userprofile = yield call(request, requestURL);
  if (!userprofile.err) {
    yield put(getUserProfileDataSuccess(userprofile));
  } else {
    yield put(getUserProfileDataFail(userprofile.err));
  }
}

/**
 * Watches for LOAD_PROFILE actions and calls getUserData when one comes in.
 * By using `takeLatest` only the result of the latest API call is applied.
 */
export function* getUserDataWatcher() {
  yield fork(takeLatest, LOAD_PROFILE, getUserData);
}

/**
 * Root saga manages watcher lifecycle
 */
export function* userData() {
  // Fork watcher so we can continue execution
  const watcher = yield fork(getUserDataWatcher);
}

// Bootstrap sagas
export default [
  userData,
];

actions.js :

(..)
export function getUserProfileData(slug) {
  return {
    type: LOAD_PROFILE,
    slug,
  };
}
(..)

routes.js

(..)
}, {
      path: '/profile/:slug',
      name: 'userProfile',
      getComponent(nextState, cb) {
        const importModules = Promise.all([
          System.import('containers/ProfilePage/actions'),
          System.import('containers/ProfilePage/reducer'),
          System.import('containers/ProfilePage/sagas'),
          System.import('containers/ProfilePage'),
        ]);

        const renderRoute = loadModule(cb);

        importModules.then(([actions, reducer, sagas, component]) => {
          injectReducer('profilePage', reducer.default);
          injectSagas(sagas.default);
          renderRoute(component);
          store.dispatch(actions.getUserProfileData(nextState.params.slug))
        });

        importModules.catch(errorLoading);
      },
    }, {
(..)
@kopax

This comment has been minimized.

Show comment
Hide comment
@kopax

kopax Jan 15, 2017

@denisflorkin how do you wait until the async operation is finish to trigger the route change ?

I've tried to play with https://github.com/jfairbank/redux-saga-router but I can't get integrated on top of react-boilerplate.

kopax commented Jan 15, 2017

@denisflorkin how do you wait until the async operation is finish to trigger the route change ?

I've tried to play with https://github.com/jfairbank/redux-saga-router but I can't get integrated on top of react-boilerplate.

@denisflorkin

This comment has been minimized.

Show comment
Hide comment
@denisflorkin

denisflorkin Jan 15, 2017

Hello @kopax, if I understand correctly, I'm not waiting. I change route immediately and the 'page' component that is the target of the route change (here: ProfilePage) handle displaying a loader if the data aren't there yet.

ProfilePage/reducer.js

(..)
const initialState = fromJS({
  profileData: false,
  isFetching: false,
  error: false,
});

function ProfilePageReducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_PROFILE:
      return state
        .set('isFetching', true)
        .set('error', false)
        .set('profileData', false);
    case LOAD_PROFILE_SUCCESS:
      return state
        .set('isFetching', false)
        .set('error', false)
        .set('profileData', action.profile);
    case LOAD_PROFILE_FAIL:
      return state
        .set('isFetching', false)
        .set('error', action.error)
        .set('profileData', false);
    default:
      return state;
  }
}

ProfilePage/index.js

(..)
  preRenderUserProfile(profileData) {
    return profileData ?
      (<UserProfile { ...profileData } />) : (<p>loading</p>)
  }

  getHelmet(profileData) {
    const username = 
      profileData ? profileData.username : 'loading'
    return (
      <Helmet
        title={ `ProfilePage : ${ username }` }
        meta={[
          { name: 'description', content: 'ProfilePage' },
        ]}
      />)
  }
  
  render() {
    const profileData = this.props.profileData
    return (
      <div>
        <FormattedMessage { ...messages.header } />
        { this.getHelmet(profileData) }
        { this.preRenderUserProfile(profileData) }
      </div>
    );
  }
(..)

denisflorkin commented Jan 15, 2017

Hello @kopax, if I understand correctly, I'm not waiting. I change route immediately and the 'page' component that is the target of the route change (here: ProfilePage) handle displaying a loader if the data aren't there yet.

ProfilePage/reducer.js

(..)
const initialState = fromJS({
  profileData: false,
  isFetching: false,
  error: false,
});

function ProfilePageReducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_PROFILE:
      return state
        .set('isFetching', true)
        .set('error', false)
        .set('profileData', false);
    case LOAD_PROFILE_SUCCESS:
      return state
        .set('isFetching', false)
        .set('error', false)
        .set('profileData', action.profile);
    case LOAD_PROFILE_FAIL:
      return state
        .set('isFetching', false)
        .set('error', action.error)
        .set('profileData', false);
    default:
      return state;
  }
}

ProfilePage/index.js

(..)
  preRenderUserProfile(profileData) {
    return profileData ?
      (<UserProfile { ...profileData } />) : (<p>loading</p>)
  }

  getHelmet(profileData) {
    const username = 
      profileData ? profileData.username : 'loading'
    return (
      <Helmet
        title={ `ProfilePage : ${ username }` }
        meta={[
          { name: 'description', content: 'ProfilePage' },
        ]}
      />)
  }
  
  render() {
    const profileData = this.props.profileData
    return (
      <div>
        <FormattedMessage { ...messages.header } />
        { this.getHelmet(profileData) }
        { this.preRenderUserProfile(profileData) }
      </div>
    );
  }
(..)
@rentrop

This comment has been minimized.

Show comment
Hide comment
@rentrop

rentrop Feb 12, 2017

@denisflorkin Thanks for sharing your code in depth. Reading this was much clearer than the doku on the topic: https://github.com/react-boilerplate/react-boilerplate/blob/master/docs/js/routing.md#dynamic-routes

rentrop commented Feb 12, 2017

@denisflorkin Thanks for sharing your code in depth. Reading this was much clearer than the doku on the topic: https://github.com/react-boilerplate/react-boilerplate/blob/master/docs/js/routing.md#dynamic-routes

@denisflorkin

This comment has been minimized.

Show comment
Hide comment
@denisflorkin

denisflorkin Feb 12, 2017

@rentrop glad this could be of help :)

denisflorkin commented Feb 12, 2017

@rentrop glad this could be of help :)

@filipveschool

This comment has been minimized.

Show comment
Hide comment
@filipveschool

filipveschool Apr 5, 2017

@denisflorkin @kylesavant Would it be possible to share your project or explain what you did? Because for some reason it still doesnt work for me.

#1693 This my new topic issue I just created with the problem and code inside. I hope maybe you would and could help me or see what I do wrong :(.

filipveschool commented Apr 5, 2017

@denisflorkin @kylesavant Would it be possible to share your project or explain what you did? Because for some reason it still doesnt work for me.

#1693 This my new topic issue I just created with the problem and code inside. I hope maybe you would and could help me or see what I do wrong :(.

@soundstripe

This comment has been minimized.

Show comment
Hide comment
@soundstripe

soundstripe Jul 17, 2017

This thread was very helpful to me...I suggest using it to amend the outdated (with broken links) routing.md README.

soundstripe commented Jul 17, 2017

This thread was very helpful to me...I suggest using it to amend the outdated (with broken links) routing.md README.

@lock

This comment has been minimized.

Show comment
Hide comment
@lock

lock bot May 29, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

lock bot commented May 29, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators May 29, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.