Skip to content

Context Issues with <Link> #982

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

Closed
sgehly opened this issue Mar 21, 2015 · 9 comments
Closed

Context Issues with <Link> #982

sgehly opened this issue Mar 21, 2015 · 9 comments

Comments

@sgehly
Copy link

sgehly commented Mar 21, 2015

I'm using React-Router in order to generate links to route to dynamically-generated routes in a dynamically generated template area. Here's the code:

`ajax('GET','/content/template/'+template+'/master.html',function(tmp){
document.getElementById('template').innerHTML = tmp;

  var mop = document.getElementsByClassName("menu");
  for (var i = 0; i < mop.length; i++) {
      if(!mop[i].getAttribute('menu-name')){
        mop[i].innerHTML = '<li><a>Menu not found.</a></li>';
        return;
      }
      var mitems = menus[mop[i].getAttribute('menu-name')];
      for(var item in mitems) { 
        React.render(<Link to={mitems[item].route}>{mitems[item].text}</Link>, getAllElementsWithAttribute('menu-name',mop[i].getAttribute('menu-name'))[0]);
      }
  }

});`

All is well, and works well, until the final line of code, namely:
React.render(<Link to={mitems[item].route}>{mitems[item].text}</Link>, getAllElementsWithAttribute('menu-name',mop[i].getAttribute('menu-name'))[0]);

where, as I have seen in #400, and #513, gives
fjbbrvi

From what I have seen, this is a bug with context, however there are no cloneWithProps() in my application, and nothing seems that out of the ordinary. Any help with workarounds would be appreciated

@agundermann
Copy link
Contributor

I'm having a hard time understanding what you're trying to do and why you'd want to render like that. How does the rest of your app work?

Generally speaking, I don't think it makes sense to load html from server, render it by setting innerHTML and then replacing several parts using React.render. You usually want to have a root element (e.g. document.body) where you render your whole React app, and then load data (not markup) via ajax to update the UI.

@sgehly
Copy link
Author

sgehly commented Mar 22, 2015

@taurose Basically this is done in this way so users are able to edit what they are familiar with, HTML, on another part of the app, and then saving it so it displays on each page.

@agundermann
Copy link
Contributor

I'm still not seeing the full picture. Do you have a top level React.render call inside Router.run? Why do you use react if your markup is (mostly?) static?

Back to the problem: As far as I understand context, it's not preserved when you do another React.render (ie. context is bound to a "render tree" produced via a single React.render call, not to single components starting another render, or the DOM). The only reliable way to pass down context is via render() in components.

Two ideas to work around that:

  1. Don't use <Link> to begin with, and create your own component which receives the router instance via props instead of context. Should be mostly copy & paste.
  2. Create a wrapper for Link which receives the router instance via props and "injects" it into context for Link (<Wrapper><Link ... /></Wrapper>). The component would look similar to this one, except that you wouldn't need contextTypes, and in getChildContext you would only need to return { router: this.props.router }.

You may also have to manually rerender those links on every route change, if you want to use active classes.

@ryanflorence
Copy link
Member

React.render(<Link to={mitems[item].route}>{mitems[item].text}</Link>, getAllElementsWithAttribute('menu-name',mop[i].getAttribute('menu-name'))[0]);

You're not going to get context there, is this code inside of react component that is inside of your app rendered in a Router.run call? If so, you could make a context wrapping component that keeps the router in context for the link.

var App = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },

  componentDidMount () {
    var router = this.context.router;
    var NewLink = React.createClass({
      childContextTypes: {
        router: React.PropTypes.func
      },
      getChildContext () {
        return {
          router: router
        }
      }
      render () {
        return <Link {...this.props}/>;
      }
    });
    fetchYourTemplateThing((tmp) => {
      // do your stuff
      React.render(<NewLink .../>, el);
    });
  }
});

@dwilt
Copy link

dwilt commented May 6, 2016

I'm having a similar problem here. I built a custom stripped-down Modal component off of the Material UI Dialog component (I probably should've named it Dialog too). I'm passing in the bio template into the Modal along with a Link component that I want to use to close the modal by redirecting to another route (/about-us). We are doing this because we want our modals to be able to be deep-linked to via a route (we may not be doing this the most effective way - if so, let me know if you know of a better one). When trying to click the close button in the modal, we get the error that @sgehly is getting (router being undefined):

screenshot 2016-05-05 22 06 51

Here is the code for my Modal class:

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import styles from './Modal.scss';

class Modal extends Component {

    _open() {
        this._scrollY = window.scrollY;
        this._mount = document.getElementById('mount');
        this._modalContainer = document.createElement('div');

        var animationListener = () => {
            this._modalContainer.classList.add('modal-entered');
            this._modalContainer.classList.remove('modal-enter');

            this._mount.classList.add('hide');
            this._modalContainer.removeEventListener('animationend', animationListener);
        };

        this._modalContainer.addEventListener('animationend', animationListener);

        this._modalContainer.classList.add('modal');

        document.body.appendChild(this._modalContainer);

        this._modalContainer.classList.add('modal-enter');
    }

    _close() {
        var animationListener = () => {
            this._modalContainer.removeEventListener('animationend', animationListener, false);

            ReactDOM.unmountComponentAtNode(this._modalContainer);
            document.body.removeChild(this._modalContainer);
            this._modalContainer = null;
        };

        this._modalContainer.addEventListener('animationend', animationListener, false);

        this._mount.classList.remove('hide');
        window.scrollTo(0, this._scrollY);

        this._modalContainer.classList.add('modal-leave');
    }

    _render() {
        if(this._modalContainer) {
            let modal = (
                <div className={this.props.classNames || ''}>
                    {this.props.children}
                </div>
            );

            ReactDOM.render(modal, this._modalContainer);
        }
    }

    componentDidUpdate() {
        var open = this.props.open;

        console.log(open);

        if (open && !this._modalContainer) {
            this._open();
        }

        if (!open && this._modalContainer) {
            this._close();
        }

        this._render();
    }

    render() {
        return null;
    }
}

export default Modal;

and the instance I'm passing in the link to:

import React, {Component} from 'react'
import Modal from '../Modal/Modal';
import Avatar from '../Avatar/Avatar';
import styles from './BioModal.scss';
import Clear from 'material-ui/lib/svg-icons/content/clear';
import { Link } from 'react-router';

class BioModal extends Component {
    render() {
        var bio = this.props.bio || {};

        var overview = bio.overview,
            description;

        if (overview) {
            let overviewMarkup = () => {
                return {
                    __html: overview
                }
            };

            description = (
                <div className={styles.description} dangerouslySetInnerHTML={overviewMarkup()}></div>
            );
        }

        var bioTemplate = (
            <div className={styles.content}>
                <div className={styles.avatarContainer}>
                    <Avatar className={styles.avatar} src={`/public/${bio.fullSize}.jpg`} alt={bio.name}/>
                </div>
                <div className={styles.mainContent}>
                    <h1 className={styles.employeeName}>{bio.name}</h1>
                    { description }
                </div>
            </div>
        );

        return (
            <Modal open={this.props.open}>
                <Link to="/about-us">
                    <Clear className={styles.closeButton} color={'#333'}/>
                </Link>

                { bioTemplate }
            </Modal>
        )
    }
}

export default BioModal;

@taion
Copy link
Contributor

taion commented May 6, 2016

You can't just render modals like that into containers and preserve context. Use an existing modal library that handles this for you, or look into how to pass through context into subtrees.

@dwilt
Copy link

dwilt commented May 6, 2016

@taion thanks, figured it out via the Context section on the React site. Thank you for pointing me in the right direction. That was something I was unaware of in React.

@horgen
Copy link

horgen commented May 11, 2017

@dwilt Could you please share the final code with the context fix? :)

@dwilt
Copy link

dwilt commented May 11, 2017

@horgen sorry man, I don't have access to the code anymore as this was over a year ago :(

@lock lock bot locked as resolved and limited conversation to collaborators Jan 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants