reactjsreact-state

Component not reflecting value of a state variable


Here is some appearently simple React code that I have in a Next.JS app. (I only put the relevant part for my issue.)

.....
const [topXplain, setTopXplain]  = useState(true);
.....
<div className="z-10 max-w-5xl w-full ... text-sm lg:flex">
  <div className="flex flex-col">
    <div>{topXplain ? "YES" : "NO"}</div>
    <ReviewSession
      userID={user!.uid}
      lang={prefLang}
      revType={revisType}
      tpXpln={true}
      tpXplFn={updateUserTopXplain}
    />
    <ReviewSession
      userID={user!.uid}
      lang={prefLang}
      revType={revisType}
      tpXpln={false}
      tpXplFn={updateUserTopXplain}
    />
    <ReviewSession
      userID={user!.uid}
      lang={prefLang}
      revType={revisType}
      tpXpln={topXplain}
      tpXplFn={updateUserTopXplain}
    />
  </div>
</div>

The problem I am having is rather puzzling. When I change the state of topXplain I can see (by the result in the web browser) the line: <div>{topXplain ? "YES" : "NO"}</div> behaving as expected. That is, it swaps from "YES" to "NO" and so on. And when the page is reloaded the display is also as expected.

The first and second ReviewSession(s) also display as expected.

On the other hand, the last ReviewSession display always starts as if topXplain was true (even when it is false).

There seem to be some inconsistency in what I see. This why I think an external look may be useful.

The two top ReviewSession (given true and false for tpXpln) show that the component (ReviewSession) is behaving properly.

The label (YES-NO) display also shows that the state of topXplain is correctly reflected.

But why is it not reflected when the last ReviewSession shows up? I must be missing some detail.

Below is the relevant part of the (ReviewSession) component code. The lengthy part having nothing to do with the problem has been removed for the sake of clarity :

'use client';

import { useState, useEffect } from 'react';
.....

var utl = require('../../Utils.tsx');


export default function ReviewSession({
  userID,
  lang,
  revType,
  tpXpln,
  tpXplFn
}: {
  userID: string,
  lang: string,
  revType: string,
  tpXpln: boolean,
  tpXplFn: (v: boolean) => void
}) {
  .....
  const [showTopXpln, setShowTopXpln] = useState(tpXpln);
  .....
  
  function checkClick() {
    tpXplFn(!showTopXpln)
    setShowTopXpln(!showTopXpln);
  } /* End of checkClick */

  return (
    <div className="flex flex-col items-center">
      {sessionReady &&
        <>
          {showTopXpln &&
            <div className="bg-amber-900 text-slate-100 text-2xl font-serif p-3 my-3">
              {utl.topExplain(lang)}
            </div>
          }

          <div className='flex justify-start items-center bg-amber-700 p-3 my-1'>
            <input
              type={"checkbox"}
              checked={showTopXpln}
              onChange={() => checkClick()}
              className="bg-cyan-200"
            />
            <label className='text-slate-100 text-base font-serif ml-4'>
              {showTopXpln
                ? utl.noTopExplain(lang)
                : utl.needTopExplain(lang)}
            </label>
          </div>
          .....
        </>
      }
      .....
    </div>
  );
}

Solution

  • The issue you're facing is related to how you're managing the state of ReviewSession internally with its initial value coming from the tpXpln prop.

    In your particular case, the showTopXpln state inside ReviewSession will only take the first value it ever receives for tpXpln to initialize its state, this is why the third ReviewSession always renders as if topXplain was true because that's the initializer value you've defined in the parent component.

    There's multiple ways to fix this, but the easiest one is to simply delegate the state management to the parent component and only use the tpXpln prop inside ReviewSession instead of using a separate state (showTopXpln) inside of that component. So basically, just remove the showTopXpln state and replace all its uses with tpXpln directly.


    If for some reason you absolutely need to have the internal state, you can sync-up both the tpXpln prop and the showTopXpln state with a useEffect like so:

    useEffect(() => {
      setShowTopXpln(tpXpln)
    }, [tpXpln])
    

    This will run each time tpXpln changes, which effectively synchronizes both values.