punkbit punkbit - 3 months ago 25
Javascript Question

react-router: get param from the router listen event fails

I'm finding that when trying to get the route parameter using

react-router
router.listen(...) it fails. By using window.location.pathname.split('route/')[1], I can get the parameter. Any tips ?

I've been trying to figure out why this happens. So far I noticed that it fails on first route change (url change) - what I mean is that, by using , my url changes from
/param/y
to
/param/x
; but the parameter is only available if I click again. I guess this may be related with my action or my component ? Or the where the listener is placed in the react lifecycle ?

Not sure if I'm declaring the eventlistener in the wrong lifecycle method or; As I've been thinking, I'm passing the routing to Store, but I'm using withRouter(Component) for this event I think. I guess I need to use the routing state from redux instead. I suppose

The component that has the listener:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { setActiveQuestion, setQuestionAnswer } from '../actions/index';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router';
import Navbar from '../containers/navbar';

class Question extends Component {
constructor(props) {
super(props);
this.getClassName = this.getClassName.bind(this);
}
componentWillMount() {
this.setEventListeners();
}

setEventListeners() {
this.props.router.listen(() => {
// using location pathname instead, since props.params fail
//let question_id = this.props.params.question_id;
let question_id = window.location.pathname.split('question/')[1]
this.props.setActiveQuestion(question_id);
});
}

setAnswer(answer_id) {
let question_id = this.props.question.id;
this.props.setQuestionAnswer(question_id, answer_id);
}

getClassName(answers, item_answer_id) {

let classes = [];

// find the answer for the active question
let answer_index = _.findIndex(answers, (answer) => {
return answer.question_id === this.props.question.id;
});

// if there's no answer yet, skip class placement
if (answer_index === -1) {
return;
}

let answer = answers[answer_index];

// Test cases
const isUserCorrect = () => {
return answer.answer_id == answer.correct_answer_id && item_answer_id == answer.correct_answer_id
}

const isUserAnswer = () => {
return answer.answer_id === item_answer_id;
}

const isCorrectAnswer = () => {
return item_answer_id == answer.correct_answer_id;
}

// Test and set the correct case classname for styling
if (isUserCorrect()) {
classes.push('user_correct_answer');
}

if (isUserAnswer()) {
classes.push('user_answer');
}

if (isCorrectAnswer()) {
classes.push('correct_answer');
}

return classes.length > 0 ? classes.join(' ') : '';

}

answersList() {
return this.props.question.answers.map((answer) => {
return <li className={ this.getClassName(this.props.answers, answer.id) } key={ answer.id } onClick={ () => this.setAnswer(answer.id) }>{ answer.text }</li>
});
}

render() {
return (
<div>
<div className='question-container'>
<h2>{ this.props.question && this.props.question.question }</h2>
<ul>
{
this.props.question &&
this.answersList()
}
</ul>
</div>
<Navbar />
</div>
);
}
}

function mapStateToProps(state, ownProps) {
return {
question: state.questions.active,
answers: state.answers
}
}

function matchDispatchToProps(dispatch) {
return bindActionCreators({
setActiveQuestion: setActiveQuestion,
setQuestionAnswer: setQuestionAnswer
}, dispatch);
}

export default connect(mapStateToProps, matchDispatchToProps)(withRouter(Question));


Here's the reducer:

import { FETCH_QUESTIONS, SET_ACTIVE_QUESTION } from '../actions/index';
import _ from 'lodash';

const INITIAL_STATE = {
loading: true,
list: [],
active: 0

};

export default function(state = INITIAL_STATE, action) {

switch (action.type) {

case FETCH_QUESTIONS:

return Object.assign({}, state, {
loading: false,
list: action.payload
});

break;

case SET_ACTIVE_QUESTION:

// retrieve the active question by the route param `question id`
let question_id = parseInt(action.payload);
let question = _.find(state.list, function (question) {
return question.id === question_id;
});

return Object.assign({}, state, {
active: question
});

break;

default:
return state;

}

};


App entry point index.js:

import React from 'react';
import ReactDOM from "react-dom";
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux'
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import routes from './config/routes';
import reducers from './reducers';
import promise from 'redux-promise';

const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const store = createStoreWithMiddleware(reducers);
const history = syncHistoryWithStore(browserHistory, store);

ReactDOM.render(
<Provider store={ store }>
<Router history={ history } routes={ routes } />
</Provider>,
document.getElementById('app')
);


The router.js file:

import { combineReducers } from 'redux';
import questionsReducer from './reducer_questions';
import answerReducer from './reducer_answers';
import { routerReducer } from 'react-router-redux'

const rootReducer = combineReducers({
questions: questionsReducer,
answers: answerReducer,
routing: routerReducer
});

export default rootReducer;

Answer

Instead of listening to the router, you might want to take a look at withRotuer.

It will give you access to the params object in your connect..

withRouter(connect(function(state, props) { 
    return { question_id: props.params.question_id }; 
})(MyComponent)

And then you can listen for componentDidMount/componentWillMount and componentWillReceiveProps(nextProps

componentWillMount() {
    this.props.setActiveQuestion(this.props.question_id);
}

componentWillReceiveProps(nextProps) {
    if (this.props.question_id != nextProps.question_id) {
        this.props.setActiveQuestion(nextProps.question_id);
    }
}

Now your component will not need to know about react-router and is more reuseable, plus with you current setup your component will never stop listening for routes changes (because of missing "router.removeListner") which could lead to problems.

A good video explaining withRouter can be found here https://egghead.io/lessons/javascript-redux-using-withrouter-to-inject-the-params-into-connected-components?course=building-react-applications-with-idiomatic-redux