Understanding React - Change state of parent component


#1

Hey,
I’m just moved to React and try to do my common Blaze (ViewModel) pattern. I have a layout with 3 sub components that could change the state of the layout component, it depends f.e. on the url. For the reactive variables I’m using Tracker.Component. Now this is my code:

layout.jsx

export class Layout extends Tracker.Component {

    constructor(props) {
        super(props);
        this.state = {
            category: null,
            posting: null,
        };
        
    }
    render() {

        return (
            <div id="layout">

                <div className="bg-1"></div>

                <div className="content-box white z-depth-1">
                    
                    <div id="start">
                        <CategoriesList layout={this}/>
                        <Postings layout={this}/>
                        <Chat layout={this}/>
                    </div>
                </div>


        </div>);
    }
};

The CateoriesList is the component that lists all categories

CategoriesList.jsx

export class CategoriesList extends Tracker.Component {

    constructor(props) {
        super(props);
        this.state = {
            categories: []
        };

        this.autorun(() => {
            this.setState({
                categories: Category.find({parent: {$exists: false}}).fetch()
            })
        });
    }
    
    

    render() {

        return (
            <div className="s s1 deep-purple accent-1">


                <div className="head deep-purple lighten-1">
                    <h5>Kategorien</h5>
                </div>


                <div className="categories">

                    {this.state.categories.map(category => <CategorieList layout={this.props.layout} mainCategory={category} key={category.id}/>)}

                </div>
            </div>

        )
    }
}

…and finally I have the component for every single category:

CategorieList.jsx

export class CategorieList extends Tracker.Component {


    constructor(props) {
        super(props);

        this.state = {
            subCats: [],
        };

        this.autorun(() => {
            this.setState({
                subCats: this.props.mainCategory.getSubs().fetch(),
                urlCategory: FlowRouter.getParam("catName")
            });
        });

        this.autorun(() => {
            if(this.props.mainCategory.id == this.state.urlCategory) {
               this.props.layout.setState({category:this.props.mainCategory.id});
            }
        });

    }



    genMeta(cat) {
        if(this.state.urlCategory == cat.id) {


            return (<Helmet title={cat.name}/>)
        }
    }

    render() {


        return (
            <div className="category-list deep-purple darken-1">

                {this.genMeta(this.props.mainCategory)}

                <a href={"/kategorie/"+this.props.mainCategory.id} className="parent">{this.props.mainCategory.name}</a> <span className="new badge">2</span>

                <ul>
                    {this.state.subCats.map((cat) => {
                   
                       return <li key={cat.id}><a href={"/kategorie/"+cat.id}>{cat.name}</a> {this.genMeta(cat)}</li>;
                       
                    })}
                </ul>
            </div>
        );
    }
};

This is what should happen: If a user visits /category/about the CategoryList component should set the state of the layout to the current category. I’m getting the error, that the state of another component can’t be set within render or a concstructor, ending in an infinite loop. So my question is now: What is the recommended pattern for this?


#2

Okay, I think that the pattern above is totally wrong in React, so if I modify the state of the layout, this will end in a whole rerender and this is an antipattern. So what would be the recommended way if I have some “global state” which some of my components should depend on? At the moment I’m trying ReactiveVar because I’m working with Tracker.Component. Is that a good idea or also totally wrong?

So I’m having a globals.js:

LayoutGlobals = new ReactiveVar({
    postingId: null,
    categoryId: null
});

…and within my component:

export class CategorieList extends Tracker.Component {
  constructor(props) {
        super(props);

        this.state = {
            subCats: [],
            active: false
        };

        this.autorun(() => {
            this.setState({
                subCats: this.props.mainCategory.getSubs().fetch(),
            });
        });

        this.autorun(() => {
            if(LayoutGlobals.get().categoryId == this.props.mainCategory.id) {
                this.setState({active:true});
            } else {
                this.setState({active:false});
            }
        });

    }
}

#3

You are closer, but probably still not quite there. Generally what you want in the react world is top-down architecture. The component at the top of the tree owns the data, and passes digestible pieces of that data down to the components below. The components below pass events and actions back up by calling functions passed as properties to them. In short, low level components (near the bottom) define the contract, and provide the actual display and event wiring. Higher level/higher order components fulfill the contract by filling the data, and providing actions to do when those events happen.


#4

Well, I just fall back to the same issue on another component.

For example, I’m having cateogry items which also have subcat items:

  1. Category A
  • Sub 1
  • Sub 2
  1. Category B
  • Sub 3
  • Sub 4

Now, if I visit Sub 4, the state of Category B and Sub 4 should be go to active.

In my sub component, my code looks like this:

export class CategorySubItem extends Tracker.Component {

constructor(props) {
    super(props);

    this.state = {
        active: false
    };

    this.autorun(() => {
        if(LayoutGlobals.get().categoryId == this.props.doc.id) {
            this.setState({active:true});
            this.props.parent.setState({active:true});
        } else {
            this.setState({active:false});
            this.props.parent.setState({active:false});
        }
    });

}

This is how I initialize the child from the parent (Category->Sub):

export class CategorieList extends Tracker.Component {


constructor(props) {
    super(props);

    this.state = {
        subCats: [],
        active: false
    };

    this.autorun(() => {
        this.setState({
            subCats: this.props.mainCategory.getSubs().fetch(),
        });
    });

}


render() {


    var defaultClass = classNames({
        "deep-purple darken-4": this.state.active,
        "deep-purple darken-1": !this.state.active,
        "category-list": true

    });


    return (

        <div className={defaultClass}>
            <a href={"/kategorie/"+this.props.mainCategory.id} className="parent">{this.props.mainCategory.name}</a>
            <span className="new badge">2</span>

            <ul>
                {this.state.subCats.map((cat) => {
                    return <CategorySubItem doc={cat} key={cat.id} parent={this}/>
                })}
            </ul>
        </div>

    );

}
};

If I change parent’s state within the child, I’m ending again in infinite loops, crashing my whole browser.


#5

Oh it’s late, I saw my mistake. If I’m visiting Sub 3, Sub 4 will set the parent again to false…as you already said, I need to handle the data a component above.