1

EDIT: I solve my issue and it is working for me now - also edited my code to reflect new changes.

I am getting this error and I am not sure what is the cause of this error.

I cannot show code as it is company's material, so I will try my best to describe it:

App.js:

`class App extends React.Component {
    constructor(props) {
        super(props);
    }

    render () {
        return (
            <div>
                <Header />
                <RouteList />
                <Footer />
            </div>
        )
    }
}`

My <RouteList /> is a a stateless function that returns all Routes for the web-application.

Header.js:

class Header extends React.Component {
    constructor (props) {
        super(props);
        this.changeHeader = this.changeHeader.bind(this);
    }

    changeHeader(headerType) {
        this.props.actions.changeHeader(headerType)
    }

    GetHeader() {
        // if-else statement which will return a different sub header class
        const HeaderType = this.props.renderHeader.headerType
        if (headerType == 'abc') {
            <aHeader changeHeader={this.changeHeader} />
        } [...] {
            // Final else block return something
        }
    }

    render () {
        return (
            <div>{this.GetHeader()}</div>
        )
    }
}

function mapStateToProps(state, ownProps) {
    return { renderHeader: state.renderHeader};
}

function mapDispatchToProps(dispatch) {
    return { actions: bindActionCreators(headerActions, dispatch) };
}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));

this.props.action.changeHeader(headerType) is an if-else statement which depending on what the value of headerType is, will fire a different action.

state.renderHeader is declared in my rootReducer.

I pass changerHeader() into individual header.js which are stateless (i.e. aHeader.js, bHeader.js...). When a navlink is clicked it will invoke the method and also route the page to another UI. This is how i embed the method into the navlink: onClick={changeHeader(input')}.

rootReducer.js

const rootReducer = combineReducers({renderHeader});
export default rootReducer;

The renderHeader is the renderHeaderReducer.

headerAction.js

export function changeHeader(headerType) {
    if (headerType == "abc") {
        return {type: type, headerType: "abc"}
    } [...] {
        // something default
    }
}

renderHeaderReducer.js

export default function renderHeaderReducer(state = initialState, action) {
switch(action.type) {
    case "abc":
        return (Object.assign({}, ...state, {headerType: action.headerType}));
    [...];
    default:
        return state;
}

}

At this point when the link is clicked, the web browser should refresh, leaving the Header in place but modifying the part. However my website goes into an infinite loop, and the error is:

Error: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

When I did a console.log to see what is going on, it seems to be looping over all the various options that i defined which will render Header.js

It turns out that the main problem was when i called my onClick method.The infinite loop that bugged my code was a result of the onClick function firing even without being clicked.

Original: onClick={this.changeHeader('abc')} New: onClick={() => changeHeader('abc')}

Refer to this post for an explanation.

Thank you.

AKJ
  • 749
  • 6
  • 29
  • You say that state.renderHeader is declared in your rootReducer; however, you should not call reducer functions from your code. Instead, you should call actions, which in turn will trigger the appropriate reducer function to modify your state. – sme Nov 01 '18 at 06:09
  • @sme i called the action `this.props.actions.changeHeader(headerType)` – AKJ Nov 01 '18 at 06:13
  • Are you calling the `renderHeader` function anywhere? I'm still trying to figure out if this is having an effect on your code. but again, if its a reducer function, it shouldn't be in your mapStateToProps() function. – sme Nov 01 '18 at 06:15
  • @sme, i am not calling my renderHeader function anywhere else. I thought mapStateToProps() function was supposed to map the reducer to the props? – AKJ Nov 01 '18 at 06:16
  • mapStateToProps should only be used to pass properties from your redux store (state), or properties passed directly to the component via `ownProps`. The action creators should be in mapDispatchToProps(), which from what I see is correct in your code. Once calling an action creator function, it will create the action, and then automatically call the appropriate reducer based on whatever `type` property the action contains. You don't need to manually call any reducers, nor do you need to pass them to any component – sme Nov 01 '18 at 06:18
  • @sme, i am not sure what you meant by by manually call any reducers. The only time i did call it is at GetHeader (see my edits), this is because i have an initialState.js which contains the initialState of the headerType. And i thought that was perfectly fine – AKJ Nov 01 '18 at 06:27
  • My main header has 3 sub headers and each sub header when clicked will send a different value to the changeHeader method to fire the action. I am wondering how come my browser will loop through all the 3 options when they havent been clicked. So i am not sure how does the state know the existence of the other options. My web browser keeps looping. lol – AKJ Nov 01 '18 at 06:28

2 Answers2

2

time for some pseudo code :)

From what I understand, you have a Header Component which is connected to a rootReducer component which contains the header for which Router Link you are on.

I have some similar code in my application where we use individual components dispatch action to update the rootReducer header. The header just listens for updates using redux and re-renders itself.

class Header extends React.Component {
     // render the header 
     render() {...}
 }
 const mapStateToProps = (state) => {
     return {
        header: state.rootReducer.header
     }
 }
 export default withRouter(connect(mapStateToProps, {})(Header));

the component

 class MySpecialRouteComponent extends React.Component {
     componentDidMount() {
         this.props.changeHeader("Special Component")
     }
     render() {...}
 }
 const mapStateToProps = (state) => {
     return {
       ...whatever 
     }
 }
 export default withRouter(connect(mapStateToProps, {changeHeader})(MySpecialRouteComponent));

you shouldn't make render do the setState in React ever!

dixitk13
  • 523
  • 4
  • 10
  • can you explain why you put the {changeHeader} in the position of the mapDispatchToProps? – AKJ Nov 01 '18 at 06:24
  • The current header that is shown should be saved in the Redux store. You should have an action creator, `changeHeader(headerType)`, that will create an action to change the header type in the store. The reducer will then update the store with the `headerType` that you passed to the action creator. – sme Nov 01 '18 at 06:28
  • I added more edits to my code showing the action and reducers – AKJ Nov 01 '18 at 06:33
  • @sme is right. the changeHeader is an action creator which dispatches and updates the state in the root reducer. And even after the update w/ more code AKJ, I feel my approach is better suited for rendering headers at least in this case. No harsh feelings :) – dixitk13 Nov 01 '18 at 06:54
  • @dixitk13 no offense taken, i really want to learn to be a better coder and you and sme have both been amazing help. Can you pls elaborate your code more? I want to see how you structure yours. I am trying sme's one now to see if it works for me. And how come i shouldnt let render do setState? What are the pros & cons? – AKJ Nov 01 '18 at 07:07
  • So I've tried doing this in the codesandbox with click buttons for you. Image the Page with different keys are the individual components. The click works inside `componentDidMount` in your case.I've used a click because I was too lazy to integrate the router and make it work. https://codesandbox.io/s/ojorpp9mo5 – dixitk13 Nov 01 '18 at 07:22
  • Thanks for the codesandbox, I think the problem here is that my onClick changeHeader is within the header itself. And the error is actually telling me that the render sholdnt be setting state. So i would like to know what is the work around it? The idea is something like this: Steak | Burger | Pasta <= When a user clicks 'steak' The header should become: Black Pepper | Chilli | Mushroom. I could easily hardcode each page with its own header, but wanted to make it nimble by using a state to modify the value of headerType which will determine the type of header to be displayed. – AKJ Nov 01 '18 at 07:29
  • AKJ, I am not quite sure I understand. I believe you are still looking for your answer. Do you mind doing a short codesandbox in order to understand better? – dixitk13 Nov 02 '18 at 06:18
1

I'll just show how I would set everything up to handle this situation.

redux/header-actions.js (call these action creators from your components):

export const changeHeader = (headerType) => {
    return {
        type: 'CHANGE_HEADER',
        payload: {
            headerType: headerType
        }
    }
}

redux/header-reducers.js (note: this will be handled when you call the action):

const INITIAL_STATE = {
    headerType: 'header-a'
};

export default function(state = INITIAL_STATE, action) {
    switch (action.type) {
        case 'CHANGE_HEADER':
            return changeHeader(state, action.payload);
        default:
            return state;
    }
}

const changeHeader = (state, payload) => {
    // this is where your app will update the state, to indicate
    // which header should be displayed:
    return {
        ...state,
        headerType: payload.headerType
    }
}

redux/index.js:

import headerReducers from './reducers/header-reducers';
import { combineReducers } from 'redux';

const allReducers = combineReducers({
    header: headerReducers
});

export default allReducers;

Now you can set up your header.js component like this:

import { changeHeader } from '../redux/header-actions';

class Header extends Component {

    render() {
        return (
            <div>{this.renderHeader()}</div>
        );
    }

    renderHeader() {
        if (this.props.headerType === 'header-a')
            return <aHeader changeHeader={this.props.changeHeader} />
        else
            return <bHeader changeHeader={this.props.changeHeader} />;
    }

}

function mapStateToProps(store, ownProps) {
    return {
        headerType: store.header.headerType
    };
}

function mapDispatchToProps(dispatch) {
    return bindActionCreators({
        changeHeader: changeHeader
    },
    dispatch);
}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));

Then in, for example, aHeader.js:

class aHeader {

    constructor() {
        super();
        this.changeHeader = this.changeHeader.bind(this);
    }

    render() {
        return <div onClick={this.changeHeader}>header a</div>;
    }

    changeHeader() {
        this.props.changeHeader('header-b'); // call the action creator here
    }
}
sme
  • 4,023
  • 3
  • 28
  • 42
  • 1
    btw there's a typo in the allReducers where it must be headerReducers instead of header-reducer. Also for this specific approach OP is following, if tomorrow he has another prop in the header which updates in his route every time hypothetically, won't his header undergo several re-renders? since whenever a render call happens, an extra render call happens in the header component since the action is dispatched? – dixitk13 Nov 01 '18 at 06:51
  • I have a question, why didn't you use Object.assign({}, payload.headerType)? I am new to ReactJS and i am trying to understand if using Object.assign() is always needed. The tutorial i follow on Pluralsight says i should. Thanks man – AKJ Nov 01 '18 at 06:52
  • @dixitk13 Its not a typo, in header-reducer.js I am exporting a default, unnamed function, so you can call it whatever you want when importing it. – sme Nov 01 '18 at 06:54
  • 1
    I meant in redux/index.js. Since you imported `headerReducers` shouldn't you be using `header: headerReducers`? – dixitk13 Nov 01 '18 at 06:56
  • @AKJ If you want to modify an object in the store/state, you should use object.assign. But its not necessary here, because I am completely overwriting the old `headerType` value with a new one, and not directly manipulating the state. – sme Nov 01 '18 at 06:56
  • @sme, for the reducer, shouldn't be return [] and not return {}? – AKJ Nov 01 '18 at 07:03
  • Its returning the state object, not an array, so it should be {} – sme Nov 01 '18 at 07:08
  • I should mention, that if you want you can simply create one reducer file/function. However, I like to split up my store to keep it more manageable (ie, having a 'section' for the header to handle the header state, etc.) – sme Nov 01 '18 at 07:22
  • I am trying to refactor my code to model yours and test it out, just want to know for aHeader.js, my original code is a link calling the onClick and not some div. That means the routing is involved in the whole mess, would it still work? – AKJ Nov 01 '18 at 07:41
  • Hmm, actually if thats the case, you may not even need to use the redux store to handle the state of the current header shown. When the router updates the URL, you can simply decide which header to render based on the current route, inside of the `` block. So you can update the `renderHeader()` function to be like so: `return ()` – sme Nov 01 '18 at 07:46
  • Haha i definitely considered that, but I was wondering if there was a way without duplicating importing headers? because for certain pages, the headers remain the same. – AKJ Nov 01 '18 at 07:56
  • You can, though you'd have to look into the React Router documentation. Though maybe it'd be better to set up the routes like this, for example: /headerA/page1, /headerA/Page2, etc. – sme Nov 01 '18 at 08:01