Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React router Link not causing component to update within nested routes

This is driving me crazy. When I try to use React Router's Link within a nested route, the link updates in the browser but the view isn't changing. Yet if I refresh the page to the link, it does. Somehow, the component isn't updating when it should (or at least that's the goal).

Here's what my links look like (prev/next-item are really vars):

<Link to={'/portfolio/previous-item'}>
    <button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
    <button className="button button-xs">Next</button>
</Link>

A hacky solution is to manaully call a forceUpate() like:

<Link onClick={this.forceUpdate} to={'/portfolio/next-item'}>
    <button className="button button-xs">Next</button>
</Link>

That works, but causes a full page refresh, which I don't want and an error:

ReactComponent.js:85 Uncaught TypeError: Cannot read property 'enqueueForceUpdate' of undefined

I've searched high and low for an answer and the closest I could come is this: https://github.com/reactjs/react-router/issues/880. But it's old and I'm not using the pure render mixin.

Here are my relevant routes:

<Route component={App}>
    <Route path='/' component={Home}>
        <Route path="/index:hashRoute" component={Home} />
    </Route>
    <Route path="/portfolio" component={PortfolioDetail} >
        <Route path="/portfolio/:slug" component={PortfolioItemDetail} />
    </Route>
    <Route path="*" component={NoMatch} />
</Route>

For whatever reason, calling Link is not causing the component to remount which needs to happen in order to fetch the content for the new view. It does call componentDidUpdate, and I'm sure I could check for a url slug change and then trigger my ajax call/view update there, but it seems like this shouldn't be needed.

EDIT (more of the relevant code):

PortfolioDetail.js

import React, {Component} from 'react';
import { browserHistory } from 'react-router'
import {connect} from 'react-redux';
import Loader from '../components/common/loader';
import PortfolioItemDetail from '../components/portfolio-detail/portfolioItemDetail';
import * as portfolioActions  from '../actions/portfolio';

export default class PortfolioDetail extends Component {

    static readyOnActions(dispatch, params) {
        // this action fires when rendering on the server then again with each componentDidMount. 
        // but not firing with Link...
        return Promise.all([
            dispatch(portfolioActions.fetchPortfolioDetailIfNeeded(params.slug))
        ]);
    }

    componentDidMount() {
        // react-router Link is not causing this event to fire
        const {dispatch, params} = this.props;
        PortfolioDetail.readyOnActions(dispatch, params);
    }

    componentWillUnmount() {
        // react-router Link is not causing this event to fire
        this.props.dispatch(portfolioActions.resetPortfolioDetail());
    }

    renderPortfolioItemDetail(browserHistory) {
        const {DetailReadyState, item} = this.props.portfolio;
        if (DetailReadyState === 'WORK_DETAIL_FETCHING') {
            return <Loader />;
        } else if (DetailReadyState === 'WORK_DETAIL_FETCHED') {
            return <PortfolioItemDetail />; // used to have this as this.props.children when the route was nested
        } else if (DetailReadyState === 'WORK_DETAIL_FETCH_FAILED') {
            browserHistory.push('/not-found');
        }
    }

    render() {
        return (
            <div id="interior-page">
                {this.renderPortfolioItemDetail(browserHistory)}
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        portfolio: state.portfolio
    };
}
function mapDispatchToProps(dispatch) {
    return {
        dispatch: dispatch
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(PortfolioDetail);

PortfolioItemDetail.js

import React, {Component} from 'react';
import {connect} from 'react-redux';
import Gallery from './gallery';

export default class PortfolioItemDetail extends React.Component {

    makeGallery(gallery) {
        if (gallery) {
            return gallery
                .split('|')
                .map((image, i) => {
                    return <li key={i}><img src={'/images/portfolio/' + image} alt="" /></li>
            })
        }
    }

    render() {
        const { item } = this.props.portfolio;

        return (
            <div className="portfolio-detail container-fluid">
                <Gallery
                    makeGallery={this.makeGallery.bind(this)}
                    item={item}
                />
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        portfolio: state.portfolio
    };
}

export default connect(mapStateToProps)(PortfolioItemDetail);

gallery.js

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

const Gallery = (props) => {

    const {gallery, prev, next} = props.item;
    const prevButton = prev ? <Link to={'/portfolio/' + prev}><button className="button button-xs">Previous</button></Link> : '';
    const nextButton = next ? <Link to={'/portfolio/' + next}><button className="button button-xs">Next</button></Link> : '';

    return (
        <div>
            <ul className="gallery">
                {props.makeGallery(gallery)}
            </ul>
            <div className="next-prev-btns">
                {prevButton}
                {nextButton}
            </div>
        </div>
    );
};

export default Gallery;

New routes, based on Anoop's suggestion:

<Route component={App}>
    <Route path='/' component={Home}>
        <Route path="/index:hashRoute" component={Home} />
    </Route>
    <Route path="/portfolio/:slug" component={PortfolioDetail} />
    <Route path="*" component={NoMatch} />
</Route>
like image 554
unleash.it Avatar asked Aug 07 '16 01:08

unleash.it


People also ask

What is the BrowserRouter /> component?

BrowserRouter: BrowserRouter is a router implementation that uses the HTML5 history API(pushState, replaceState and the popstate event) to keep your UI in sync with the URL. It is the parent component that is used to store all of the other components.

Does React allow nested routes?

React Router version 6 makes it easy to nest routes. Nested routes enables you to have multiple components render on the same page with route parity. This is useful for app experiences where you want the user to be able to "drill down" into content and not lose their way, such as in forums or blogs.

What is the difference between route and Link in React?

So in a nutshell, the Link component is responsible for the transition from state to state (page to page), while the Route component is responsible to act as a switch to display certain components based on route state.

Is React router deprecated?

This feature has been deprecated because the new structure of Routes is that they should act like components, so you should take advantage of component lifecycle methods instead.


1 Answers

componentWillReceiveProps is the answer to this one, but it's a little annoying. I wrote a BaseController "concept" which sets a state action on route changes EVEN though the route's component is the same. So imagine your routes look like this:

<Route path="test" name="test" component={TestController} />
<Route path="test/edit(/:id)" name="test" component={TestController} />
<Route path="test/anything" name="test" component={TestController} />

So then a BaseController would check the route update:

import React from "react";

/**
 * conceptual experiment
 * to adapt a controller/action sort of approach
 */
export default class BaseController extends React.Component {


    /**
     * setState function as a call back to be set from
     * every inheriting instance
     *
     * @param setStateCallback
     */
    init(setStateCallback) {
        this.setStateCall = setStateCallback
        this.setStateCall({action: this.getActionFromPath(this.props.location.pathname)})
    }

    componentWillReceiveProps(nextProps) {

        if (nextProps.location.pathname != this.props.location.pathname) {
            this.setStateCall({action: this.getActionFromPath(nextProps.location.pathname)})
        }
    }

    getActionFromPath(path) {

        let split = path.split('/')
        if(split.length == 3 && split[2].length > 0) {
            return split[2]
        } else {
            return 'index'
        }

    }

    render() {
        return null
    }

}

You can then inherit from that one:

import React from "react"; import BaseController from './BaseController'

export default class TestController extends BaseController {


    componentWillMount() {
        /**
         * convention is to call init to
         * pass the setState function
         */
        this.init(this.setState)
    }

    componentDidUpdate(){
        /**
         * state change due to route change
         */
        console.log(this.state)
    }


    getContent(){

        switch(this.state.action) {

            case 'index':
                return <span> Index action </span>
            case 'anything':
                return <span>Anything action route</span>
            case 'edit':
                return <span>Edit action route</span>
            default:
                return <span>404 I guess</span>

        }

    }

    render() {

        return (<div>
                    <h1>Test page</h1>
                    <p>
                        {this.getContent()}
                    </p>
            </div>)
        }

}
like image 123
chickenchilli Avatar answered Oct 08 '22 14:10

chickenchilli