Search code examples
javascriptcssreactjstypescriptstyled-components

Why my styled component className logs reflect only final state?


Here is my styled components version "@types/styled-components": "^5.1.26"

and I'll give you my example code.

// index.tsx
import React, { useEffect, useState } from 'react';
import { ProfileHomeContainer } from './styled';

const ProfileHome: React.FC = () => {
  const [test, setTest] = useState(false);

  useEffect(() => {
    const my = document.getElementById('test');
    console.log(my);
    console.log(test);
    setTest(true);
  }, [test]);

  return (
    <ProfileHomeContainer id='test' test={test ? 1 : 0}>
    </ProfileHomeContainer>
  )
}


// styled.ts
import styled from 'styled-components';

interface ProfileHomeContainerProps {
  test: number;
}

const ProfileHomeContainer = styled.div<ProfileHomeContainerProps>`
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  background: rgba(0, 0, 0, 0.9);
  z-index: 1101;
  opacity: ${(props) => (props.test ? '1' : '0')};
  transition: 1s;
`;

and There are two Global CSS classes for ProfileHomeContainer.

// which has opacity: 0
.KBlzF{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;position:fixed;top:0;left:0;width:100%;height:100vh;background:rgba(0,0,0,0.9);z-index:1101;opacity:0;-webkit-transition:1s;transition:1s;}


// has opacity: 1
.goFgwQ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;position:fixed;top:0;left:0;width:100%;height:100vh;background:rgba(0,0,0,0.9);z-index:1101;opacity:1;-webkit-transition:1s;transition:1s;}

and here is my console logs

<div id="test" class="sc-crXcEl goFgwQ"> // result of console.log(my)
false // result of console.log(test)
<div id="test" class="sc-crXcEl goFgwQ">
true

Here is my question. Why my component rendered with goFgwQ first? I think it should be like this.

<div id="test" class="sc-crXcEl KBlzF">
false
<div id="test" class="sc-crXcEl goFgwQ">
true

Because, when useEffect run first time, I have false state value for test.

Here is my real code in github

index.tsx

styled.ts


Solution

  • This is an "artifact" due to the console.log slight delay, and depending on its implementation, also worsened by its lazy access.

    As you probably noticed, your transition does work, which means that your styled div (<ProfileHomeContainer>) did render first as opacity 0, then after at least an animation frame, was modified by the state change to get the other class version with opacity 1 (as expected from your useEffect).

    The counter-intuitive part is that these 2 steps are not reflected by the console.log(element), which display only the latter class name.

    Here is a playground on CodeSandbox: https://codesandbox.io/s/mutable-surf-s1p4lt?file=/src/App.tsx

    I intentionally delayed the transition to test === true to better see the effect of the browser console lazy access:

      useEffect(() => {
        const my = document.getElementById("test");
        console.log(my);
        console.log(test);
        // Delayed toggling of test state
        setTimeout(() => setTest(true), 1000);
        //setTest(true);
      }, [test]);
    
    

    Open both the CodeSandbox and the browser console's (they may have slightly different behaviour):

    • Thanks to the delaying, we now have the 2 different class names correctly appearing in the consoleinitial log
    • In the browser console, now click to expand the element logged before the false log of test state value
    • Its class name will change for the one with opacity 1! log changed after expanding (I am using Chrome browser)

    This shows that the browser console displays objects in their state when you expand them (lazy access), which may differ from the moment you executed console.log, and even from the preview already displayed in the console!

    Note that this effect does not play in CodeSandbox console: its implementation is slightly different.

    Now to come back to your original issue, let's remove the delay (setTimeout).

    We get back the original behaviour: all element logs display only the "final" class name (with opacity 1). This is very probably because by the time the console.log accesses the element to display its information, React already performed a new rendering pass (due to the setTest(true) in the useEffect).

    This shows BTW that this element is actually the same between re-renders: React does a good job at re-using DOM elements.

    We can see this last effect by forcing React to use a different DOM element, e.g. by specifying a changing key prop:

    <ProfileHomeContainer id="test" test={test ? 1 : 0} key={test.toString()}>
    

    https://codesandbox.io/s/interesting-parm-mwgs9t?file=/src/App.tsx

    Because the value passed to the key prop changes, React no longer re-uses the DOM element, but creates a new element, and the log is now as expected: logs with changing DOM elements because of key prop

    But this also breaks the transition effect, which must be applied on the same element to work.