简体   繁体   中英

React does not render correct conditional element

I'm using latest version of react with latest mobx for state.

I was trying to make an error (or any message) appear once and then disappear on action. Using just the state I did not think of easy way to do that so what I "invented" is the following:

In mobx state I keep whether I have an error, the error and it's description. In simple JS object (which is not reactive to changes) I keep whether this error is shown or not. So when the error is shown for the first time the layout renders - it saves in the simple JS object that error is shown and the second time if the error is shown - remove it from mobx (thus updating the UI). This was my workaround using reactive and non-reactive state for one-time messages.

However trying to implement that I encountered the following behaviour:

console.log('Render MainLayout...');

if (state.hasError || state.hasSuccess) {
    console.log('Has message');
    if (NonReactiveState.messageDisplayed) {
        console.log('Message already displayed - hiding');
        state.clearMessages();
    } else {
        console.log('Displaying message...');
        NonReactiveState.messageDisplayed = true;
    }
}

if (state.hasError) {
    console.log('Displaying error...');
    dummy = <div className="dummy-element-error"></div>
    errorAlert = (
        <Alert variant="danger">
            <Alert.Heading>{state.errorTitle}</Alert.Heading>
            <p>{state.errorDescription}</p>
        </Alert>
    )
} else {
    console.log('No error to display');
    dummy = <div className="dummy-element-no-error"></div>
}

if (state.hasSuccess) {
    console.log('Displaying success...');
    successAlert = (
        <Alert variant="success">
            <Alert.Heading>{state.successTitle}</Alert.Heading>
            <p>{state.successDescription}</p>
        </Alert>
    )
}

console.log('error el', errorAlert);
console.log('success el', successAlert);
console.log(state);

After rendering with error in the console I have the following:

安慰

And the jsx looks like this:

...
<div className="content">
    <div className="container-fluid">
        <div className="invalid-class-bla">
            {dummy}
            {errorAlert}
            {successAlert}
        </div>
        ...
    </div>
</div>
...

However I cannot wrap my head around why my HTML does not display the alert and instead has the following:

html

It does have the console log from the correct IF block. It does show that the errorAlert is not empty. But at the end in the HTML error alert is empty and the 'debug' element contains the no-error class.

I might be missing something really dump here, but I'm stuck on that.

At the end of the day - my "genius" idea to use non-reactive state for one-time messages will not work with React. What I found out after a LOT of debugging is that React secretly re-renders the components for various reasons, hiding all console.log and other outputs.

I set up a static render counter which increments by 1 each time the function is called. I generate that number once at the start of the functional component and I print that number both in console.log and in the component JSX.

Counter looks like this:

let id = 0;

const uniqueId = () => {
    return 'id-' + id++;
};

export default uniqueId;

Simple component:

const Debug = () => {
    const id = uniqueId();
    console.log('Render id: ' + id);

    return (
        <div className="render-id">
            {id}
        </div>
    );
};

export default Debug;

The result is that console.log in the component is always 1-2 renders behind the actual content of the JSX that is rendered. Meaning that the component was rendered in the background with output disabled after the initial render which I see the console.log print.

控制台计数器 jsx计数器

This effectively means that it executes the 'one-time' logic multiple times and that breaks the whole idea of my non-reactive state, which is based on real render counts.

Guess I have to think of another idea for single-time message rendering that is based on reactive state, so that React can manage that internally during the hidden re-renders.

EDIT

Ok after even more debugging I found that my problems come from React's strict mode in development mode. The documentations explicitly states:

Strict mode can't automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor, render, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useState, useMemo, or useReducer

Without strict mode the functionality works as intended...

EDIT 2

I was wrong with my initial statement that the non-reactive state is not working. It is working, but any change to it HAS to happen in a useEffect hook so it doesn't execute during the Strict Mode double execution. So my mistake was that I was trying to make changes (side effects) directly in the body of the functional component. That is equal to the render() function in the class component. This is incorrect.

Any changes to anything, doesn't matter if it's reactive or not has to happen in a useEffect hook, otherwise the results are unpredictable and random. Basic mistake but breaking a fundamental principle.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM